🔄 卡若AI 同步 2026-03-03 22:01 | 更新:Cursor规则、金仓、水溪整理归档、卡木、总索引与入口、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个
This commit is contained in:
@@ -24,6 +24,8 @@ alwaysApply: true
|
||||
|
||||
**MAX Mode**:卡若AI 每次调用均为 MAX Mode,定义在**卡若AI 本体** `BOOTSTRAP.md` 第四节(MAX Mode)与第五节(执行流程),不在此重复;本文件仅补充 Cursor 特有行为。
|
||||
|
||||
**多线程并行(1~6)**:当任务可拆为多个相对独立的子任务时,**优先并行处理**。由卡若AI 划定各子任务边界与归属域(五行/成员/技能),一次派发 **1~6 个**并行子任务(如 Cursor 内使用 mcp_task 等多 agent 能力);各子任务在各自边界内独立判断、全力处理,完成后汇总结果。详见 `BOOTSTRAP.md` 四.1 与 `运营中枢/参考资料/多线程并行处理规范.md`。
|
||||
|
||||
## 异常处理与红线(强制)
|
||||
|
||||
执行时遵守 `运营中枢/参考资料/卡若AI异常处理与红线.md`:未匹配→推荐 2~3 技能或学习扩展;API 失败→搜索并循环直到成功;多技能→合并不让用户选;复盘遗漏→强制补发。**红线**:不改变卡若AI 整体结构、不导致电脑无法启动、不删除重大文件。
|
||||
@@ -32,6 +34,7 @@ alwaysApply: true
|
||||
|
||||
### 第一步~第四步(执行流程与 MAX Mode)
|
||||
- 执行流程、思考与拆解、验证与复盘**以 `BOOTSTRAP.md` 第四节(MAX Mode)与第五节(执行流程)为准**,卡若AI 每次调用均为 MAX Mode,此处不重复。
|
||||
- **强制**:每次对话必须先**在对话中以详细文字展示**「思考结果 + 任务拆解 + 执行计划」,展示完毕后再执行;禁止只写提纲或省略,禁止不展示直接动手。
|
||||
|
||||
### 复盘格式(Cursor 内强制)
|
||||
- **卡若AI 内所有对话的 AI 回复一律采用「复盘形式」——此为强制行为,无例外。** 按 `运营中枢/参考资料/卡若复盘格式_固定规则.md`:
|
||||
@@ -59,9 +62,10 @@ alwaysApply: true
|
||||
### 复盘(所有对话强制,卡若AI 统一回复形式)
|
||||
- **卡若AI 内所有对话:AI 的回复一律用复盘形式。** 复盘格式**永远只有一种**:完整复盘;**复盘块内不用表格**;每块用**小图标识别**(🎯📌💡📝▶);标题必须写**具体日期 + 具体时间**(真实 YYYY-MM-DD HH:mm);**能加颜色则加**(日期时间、各块标题)。详见 `运营中枢/参考资料/卡若复盘格式_固定规则.md`。
|
||||
|
||||
### 终端命令与常规操作
|
||||
- **终端命令**:直接执行不询问,50 字内说明后执行
|
||||
- **常规操作**:优先用命令行完成;执行前先搜索本仓库(SKILL、参考资料、脚本、已有文档)及网上是否有**类似/现成的命令或流程**,有则**直接按现成方式帮用户完成**,不重复造轮子、不向用户提问「是否执行」「请确认」
|
||||
### 终端命令与常规操作(强制:需终端的一律直接执行)
|
||||
- **需在终端执行的,一律由 AI 直接执行,不把命令交给用户自己跑。** 包括但不限于:下载/同步脚本(如从 GitHub 拉最新)、部署脚本、构建、同步 Gitea、运行 SKILL 内脚本等。不输出「请你在终端执行」「请运行以下命令」让用户自己复制执行。
|
||||
- **终端命令**:直接执行不询问,50 字内说明后执行。
|
||||
- **常规操作**:优先用命令行完成;执行前先搜索本仓库(SKILL、参考资料、脚本、已有文档)及网上是否有**类似/现成的命令或流程**,有则**直接按现成方式帮用户完成**,不重复造轮子、不向用户提问「是否执行」「请确认」。
|
||||
|
||||
### 工作台路径
|
||||
- `/Users/karuo/Documents/个人/卡若AI/`
|
||||
|
||||
@@ -58,6 +58,8 @@ kr宝塔: qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT
|
||||
5. **Skill 迭代**:每次有新的配置、教训、流程变更时,必须同步更新本 SKILL.md 或 references,保证下次调用时信息一致。
|
||||
6. **卡若AI 复盘**:每次任务结束必须用卡若AI 复盘格式收尾(目标·结果·达成率、过程、反思、总结、下一步)。
|
||||
7. **站点/Nginx/SSL 类操作 · 默认宝塔 API,被拒则降级**:凡涉及「添加站点、Nginx 反代、重载、申请 SSL、开启 HTTPS」等,**默认一律先用宝塔 API**(本机执行对应脚本,需执行机公网 IP 已加入该面板「设置 → API 接口」白名单)。若调用被拒绝(如 **IP 校验失败**、连接超时等),则**降级一:用 SSH 直接操作**(在服务器上写 Nginx 配置、重载、或在服务器内用 127.0.0.1 调宝塔 API 免白名单)。若 **SSH 操作不了**(连接被关、风控、无密钥等),则**降级二:用腾讯云 API**(TAT 下发命令、或 CVM/轻量 API 等)在服务器内完成同样操作。以后碰到此类问题,直接按「宝塔 API → SSH → 腾讯云 API」顺序执行,不反复询问用户。
|
||||
8. **执行形式强制顺序**:宝塔服务器上任何操作,**一律按「宝塔 API → SSH → TAT」顺序尝试**。能通过宝塔 API 完成的先用 API;API 不可用(超时、IP 校验失败等)再用 SSH 在机内执行;SSH 不可用再用腾讯云 TAT。不得跳过顺序。
|
||||
9. **前置检查(强制)**:每次对服务器做**修改类操作**(改配置、改站点、改 Node 项目、重启服务等)之前,**必须先检查目标项目及周边项目/应用**(如通过宝塔 API `get_project_list` 看 Node 项目状态、或 SSH 看进程与端口),确认修改不会导致其他应用不可用后再执行;执行后再次确认目标与周边状态正常。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,6 +3,34 @@
|
||||
> 当一种方式失败时,依次尝试其他方式。终极备选:**宝塔面板 → 终端**(无需 SSH)。
|
||||
> **存客宝 SSH 修复**:在存客宝宝塔终端执行 `scripts/存客宝_SSH修复_宝塔终端执行.sh` 内容。
|
||||
|
||||
## 启动 SSH 并保证连接成功(kr宝塔 43.139.27.93)
|
||||
|
||||
按顺序执行即可:
|
||||
|
||||
**① 安全组放行 22、22022**(在卡若AI 项目根目录执行):
|
||||
|
||||
```bash
|
||||
cd "/Users/karuo/Documents/个人/卡若AI"
|
||||
.venv_tencent/bin/python3 "01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_kr宝塔安全组放行SSH.py"
|
||||
```
|
||||
|
||||
**② 在服务器上启动 sshd**(二选一):
|
||||
|
||||
- **能连上 SSH 时**:`ssh -p 22022 root@43.139.27.93` 登录后执行
|
||||
`systemctl enable sshd && systemctl start sshd && systemctl status sshd`
|
||||
- **连不上时**:用 **宝塔面板终端**:打开 https://43.139.27.93:9988 → 登录 → 终端 → 执行上述三条命令。
|
||||
|
||||
**③ 测试连接**:
|
||||
|
||||
```bash
|
||||
ssh -p 22022 -o StrictHostKeyChecking=no root@43.139.27.93
|
||||
# 密码:Zhiqun1984(首字母大写 Z)
|
||||
```
|
||||
|
||||
若仍失败,见下方「Connection closed 原因与处理」和「终极备选:宝塔面板终端」。
|
||||
|
||||
---
|
||||
|
||||
## 零、SSH IP 被封禁防护(2026-02-23 已配置)
|
||||
|
||||
**问题**:sshpass/密钥连接被 `Connection closed by remote host`,原因是外部暴力破解(19,690 次错误尝试)占满 sshd 连接池。
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
> 当 TAT 超时或 Node 批量启动失败时,在**宝塔面板终端**执行修复脚本,无时间限制。
|
||||
|
||||
**执行顺序(强制)**:宝塔 API 优先 → SSH → TAT。每次修改前**必须先检查目标项目及周边项目/应用**,确认不影响其他应用后再执行。详见 SKILL.md 强制规则 8、9。
|
||||
|
||||
---
|
||||
|
||||
## 一、执行步骤
|
||||
@@ -52,3 +54,42 @@
|
||||
|
||||
- 本地:`01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_运行堵塞与Node深度修复_宝塔终端执行.sh`
|
||||
- 宝塔 API 密钥:`qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT`(脚本内已配置)
|
||||
|
||||
---
|
||||
|
||||
## 四、wzdj.quwanzhi.com 单独修复(启动失败:Cannot find module '/www/wwwroot/self/wzdj')
|
||||
|
||||
**原因**:宝塔 Node 项目把目录路径当模块执行(`node /www/wwwroot/self/wzdj`),应改为在项目目录下执行 `PORT=3055 pnpm start`(Nginx 反代 3055)。若出现 **EADDRINUSE: 3055**,先在服务器执行 `fuser -k 3055/tcp` 释放端口再启动。
|
||||
|
||||
**执行顺序(强制)**:宝塔 API → SSH → TAT。每次修改前先检查本项目及周边 Node 项目状态,防止影响其他应用。
|
||||
|
||||
**方式一(推荐)**:本机执行完整流程脚本(前置检查 + API 停/启 + SSH 修复,失败则 TAT)
|
||||
```bash
|
||||
cd /Users/karuo/Documents/个人/卡若AI
|
||||
python3 "01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_wzdj修复_完整流程.py"
|
||||
```
|
||||
|
||||
**方式二**:本机执行 TAT 脚本(需腾讯云 API 凭证,当 SSH 不可用时)
|
||||
|
||||
```bash
|
||||
cd /Users/karuo/Documents/个人/卡若AI
|
||||
python3 "01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_修复wzdj启动.py"
|
||||
```
|
||||
|
||||
**方式二**:SSH 或宝塔终端执行
|
||||
|
||||
1. SSH:`sshpass -p 'Zhiqun1984' ssh -p 22022 -o PubkeyAuthentication=no root@43.139.27.93`
|
||||
2. 上传或在服务器上创建脚本后执行:
|
||||
`bash /root/kr宝塔_仅修复wzdj_宝塔终端执行.sh`
|
||||
3. 或打开本地 `scripts/kr宝塔_仅修复wzdj_宝塔终端执行.sh`,全选复制,在宝塔终端粘贴执行。
|
||||
|
||||
脚本会:停止 wzdj → 修复 site.db 与 wzdj.sh 启动命令 → 再启动 wzdj。
|
||||
|
||||
**若仍启动失败(推荐·一步到位)**:在宝塔面板里**手动改启动命令**:
|
||||
1. 宝塔 → **网站** → **Node 项目** → 找到 **wzdj** → 点 **设置**(或「编辑」)。
|
||||
2. 找到「**启动命令**」或「**运行命令**」输入框,**整段替换**为(复制下面一行):
|
||||
```bash
|
||||
cd /www/wwwroot/self/wzdj && (PORT=3055 pnpm start 2>/dev/null || PORT=3055 npm run start)
|
||||
```
|
||||
3. **保存** → 回到 Node 项目列表 → 对 wzdj 点 **启动**。
|
||||
这样不依赖脚本是否在机内执行成功,直接改面板配置即可让文字电竞站点跑起来。
|
||||
|
||||
204
01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_wzdj修复_完整流程.py
Normal file
204
01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_wzdj修复_完整流程.py
Normal file
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""wzdj.quwanzhi.com 修复完整流程:前置检查 → 宝塔 API → SSH → TAT,直至网站可访问。"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import ssl
|
||||
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
KR_PANEL = "https://43.139.27.93:9988"
|
||||
KR_API_KEY = "qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT"
|
||||
KR_SSH = "root@43.139.27.93"
|
||||
KR_SSH_PORT = "22022"
|
||||
KR_SSH_PASS = "Zhiqun1984"
|
||||
WZDJ_DOMAIN = "wzdj.quwanzhi.com"
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
FIX_SH = os.path.join(SCRIPT_DIR, "kr宝塔_仅修复wzdj_宝塔终端执行.sh")
|
||||
|
||||
|
||||
def bt_sign():
|
||||
t = int(time.time())
|
||||
token = hashlib.md5((str(t) + hashlib.md5(KR_API_KEY.encode()).hexdigest()).encode()).hexdigest()
|
||||
return {"request_time": t, "request_token": token}
|
||||
|
||||
|
||||
def bt_post(path, data=None):
|
||||
pl = bt_sign()
|
||||
if data:
|
||||
pl.update(data)
|
||||
req = urllib.request.Request(KR_PANEL + path, data=urllib.parse.urlencode(pl).encode(), method="POST")
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
|
||||
def bt_stop_wzdj():
|
||||
"""宝塔 API 停止 wzdj"""
|
||||
try:
|
||||
bt_post("/project/nodejs/stop_project", {"project_name": "wzdj"})
|
||||
print(" API 已停止 wzdj")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(" API 停止 wzdj 失败:", e)
|
||||
return False
|
||||
|
||||
|
||||
def pre_check():
|
||||
"""前置检查:本项目及周边 Node 项目状态(优先宝塔 API)"""
|
||||
print("【前置检查】目标项目 wzdj 及周边 Node 项目状态")
|
||||
try:
|
||||
r = bt_post("/project/nodejs/get_project_list")
|
||||
items = r.get("data") or r.get("list") or []
|
||||
if not items:
|
||||
print(" API 返回无项目列表")
|
||||
return None
|
||||
for it in items:
|
||||
name = it.get("name") or it.get("project_name") or ""
|
||||
run = it.get("run", False)
|
||||
path = it.get("path") or it.get("project_path") or ""
|
||||
print(" -", name, "运行:" if run else "未运行", path[:50] if path else "")
|
||||
wzdj = next((x for x in items if (x.get("name") or x.get("project_name")) == "wzdj"), None)
|
||||
return {"ok": True, "wzdj": wzdj, "all": items}
|
||||
except Exception as e:
|
||||
print(" 宝塔 API 不可用:", str(e)[:80])
|
||||
return None
|
||||
|
||||
|
||||
def fix_via_ssh():
|
||||
"""通过 SSH 在服务器上执行修复脚本"""
|
||||
if not os.path.isfile(FIX_SH):
|
||||
print(" 修复脚本不存在:", FIX_SH)
|
||||
return False
|
||||
try:
|
||||
with open(FIX_SH, "r", encoding="utf-8") as f:
|
||||
script = f.read()
|
||||
cmd = [
|
||||
"sshpass", "-p", KR_SSH_PASS,
|
||||
"ssh", "-p", KR_SSH_PORT,
|
||||
"-o", "StrictHostKeyChecking=no", "-o", "PubkeyAuthentication=no",
|
||||
KR_SSH, "bash -s"
|
||||
]
|
||||
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
out, _ = p.communicate(input=script, timeout=120)
|
||||
print(out[:2000] if out else "(无输出)")
|
||||
return p.returncode == 0
|
||||
except FileNotFoundError:
|
||||
print(" sshpass 未安装,跳过 SSH")
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
print(" SSH 执行超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(" SSH 失败:", e)
|
||||
return False
|
||||
|
||||
|
||||
def fix_via_tat():
|
||||
"""通过 TAT 在服务器上执行修复"""
|
||||
tat_script = os.path.join(SCRIPT_DIR, "腾讯云_TAT_kr宝塔_修复wzdj启动.py")
|
||||
if not os.path.isfile(tat_script):
|
||||
print(" TAT 脚本不存在:", tat_script)
|
||||
return False
|
||||
try:
|
||||
venv_py = os.path.join(SCRIPT_DIR, ".venv_tx", "bin", "python")
|
||||
karuo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(SCRIPT_DIR))))
|
||||
py = venv_py if os.path.isfile(venv_py) else sys.executable
|
||||
p = subprocess.run([py, tat_script], capture_output=True, text=True, timeout=90, cwd=karuo_root)
|
||||
if p.stdout:
|
||||
print(p.stdout[:1500])
|
||||
if p.returncode != 0 and p.stderr:
|
||||
print("stderr:", p.stderr[:500])
|
||||
return p.returncode == 0
|
||||
except Exception as e:
|
||||
print(" TAT 失败:", e)
|
||||
return False
|
||||
|
||||
|
||||
def start_wzdj_api():
|
||||
"""宝塔 API 启动 wzdj"""
|
||||
try:
|
||||
r = bt_post("/project/nodejs/start_project", {"project_name": "wzdj"})
|
||||
ok = r.get("status") or ("成功" in str(r.get("msg", "")))
|
||||
print(" API 启动 wzdj:", "成功" if ok else r.get("msg", r))
|
||||
return ok
|
||||
except Exception as e:
|
||||
print(" API 启动失败:", e)
|
||||
return False
|
||||
|
||||
|
||||
def verify_site():
|
||||
"""验证站点可访问"""
|
||||
try:
|
||||
req = urllib.request.Request("https://" + WZDJ_DOMAIN, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
code = r.getcode()
|
||||
print(" %s HTTP %s" % (WZDJ_DOMAIN, code))
|
||||
return 200 <= code < 400
|
||||
except Exception as e:
|
||||
print(" 访问 %s 失败: %s" % (WZDJ_DOMAIN, e))
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
# 1) 前置检查
|
||||
pre_check()
|
||||
print()
|
||||
|
||||
# 2) 先尝试 API 停止 wzdj(便于后续改配置)
|
||||
print("【停止 wzdj】优先宝塔 API")
|
||||
bt_stop_wzdj()
|
||||
print()
|
||||
|
||||
# 3) 修复:优先 SSH,失败再用 TAT(改 site.db / wzdj.sh 只能机内执行)
|
||||
print("【修复】按顺序尝试:SSH → TAT")
|
||||
if fix_via_ssh():
|
||||
print(" 已通过 SSH 完成修复")
|
||||
else:
|
||||
print(" 改用 TAT 执行修复")
|
||||
fix_via_tat()
|
||||
print()
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
# 4) 启动 wzdj:优先宝塔 API
|
||||
print("【启动 wzdj】优先宝塔 API")
|
||||
if not start_wzdj_api():
|
||||
print(" API 启动失败,修复脚本内已含启动步骤,请稍后查看面板")
|
||||
print()
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
# 5) 再次检查项目与周边
|
||||
print("【修复后检查】目标及周边项目")
|
||||
pre_check()
|
||||
print()
|
||||
|
||||
# 6) 验证站点
|
||||
print("【验证站点】")
|
||||
ok = verify_site()
|
||||
if ok:
|
||||
print(" 结果: wzdj.quwanzhi.com 可正常访问")
|
||||
else:
|
||||
print(" 若仍不可访问,请到宝塔面板查看 Node 项目 wzdj 状态与日志")
|
||||
# 写入工作台结果文件
|
||||
try:
|
||||
karuo = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(SCRIPT_DIR)))))
|
||||
out_path = os.path.join(karuo, "运营中枢", "工作台", "wzdj_flow_result.txt")
|
||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write("wzdj 修复流程 %s\n" % time.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
f.write("站点可访问: %s\n" % ok)
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
91
01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_仅修复wzdj_宝塔终端执行.sh
Normal file
91
01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_仅修复wzdj_宝塔终端执行.sh
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
# 仅修复 wzdj.quwanzhi.com 启动失败(Cannot find module '/www/wwwroot/self/wzdj')
|
||||
# 在宝塔终端或 SSH 执行:bash 本脚本 或 直接粘贴内容执行
|
||||
set -e
|
||||
echo "【1】停止 wzdj"
|
||||
python3 -c "
|
||||
import hashlib,json,time,urllib.request,urllib.parse,ssl
|
||||
ssl._create_default_https_context=ssl._create_unverified_context
|
||||
P,K='https://127.0.0.1:9988','qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT'
|
||||
def sg():
|
||||
t=int(time.time())
|
||||
return {'request_time':t,'request_token':hashlib.md5((str(t)+hashlib.md5(K.encode()).hexdigest()).encode()).hexdigest()}
|
||||
def post(u,d=None):
|
||||
pl=sg()
|
||||
if d: pl.update(d)
|
||||
r=urllib.request.Request(P+u,data=urllib.parse.urlencode(pl).encode())
|
||||
return json.loads(urllib.request.urlopen(r,timeout=15).read().decode())
|
||||
try:
|
||||
post('/project/nodejs/stop_project',{'project_name':'wzdj'})
|
||||
print('stop wzdj ok')
|
||||
except Exception as e: print('stop',e)
|
||||
" 2>/dev/null || true
|
||||
|
||||
echo "【2】修复 site.db 中 wzdj 的 project_script/run_cmd"
|
||||
python3 -c "
|
||||
import json,sqlite3
|
||||
db='/www/server/panel/data/db/site.db'
|
||||
path='/www/wwwroot/self/wzdj'
|
||||
cmd='cd %s && (PORT=3055 pnpm start 2>/dev/null || PORT=3055 npm run start)' % path
|
||||
conn=sqlite3.connect(db)
|
||||
c=conn.cursor()
|
||||
c.execute(\"SELECT id,name,path,project_config FROM sites WHERE name='wzdj' AND project_type='Node'\")
|
||||
row=c.fetchone()
|
||||
if row:
|
||||
sid,nm,oldpath,cfg=row[0],row[1],row[2] or '',row[3] or '{}'
|
||||
try: cfg=json.loads(cfg)
|
||||
except: cfg={}
|
||||
cfg['project_script']=cfg['run_cmd']=cmd
|
||||
cfg['path']=path
|
||||
c.execute('UPDATE sites SET path=?, project_config=? WHERE id=?',(path,json.dumps(cfg,ensure_ascii=False),sid))
|
||||
conn.commit()
|
||||
print('site.db wzdj fixed')
|
||||
else:
|
||||
print('wzdj not found in sites')
|
||||
conn.close()
|
||||
"
|
||||
|
||||
echo "【3】修复 wzdj.sh 启动脚本"
|
||||
SH=/www/server/nodejs/vhost/scripts/wzdj.sh
|
||||
if [ -f \"$SH\" ]; then
|
||||
python3 -c "
|
||||
p='/www/server/nodejs/vhost/scripts/wzdj.sh'
|
||||
path='/www/wwwroot/self/wzdj'
|
||||
new_cmd='cd %s && (PORT=3055 pnpm start 2>/dev/null || PORT=3055 npm run start)' % path
|
||||
with open(p,'r') as f: lines=f.readlines()
|
||||
out=[]
|
||||
for line in lines:
|
||||
# 仅替换“执行该路径”的行:含路径且含 node/exec/$(避免改 export 等)
|
||||
if '/www/wwwroot/self/wzdj' in line and not line.strip().startswith('#') and ('node' in line.lower() or 'exec' in line or '\$' in line):
|
||||
out.append(' ' + new_cmd + '\n')
|
||||
else:
|
||||
out.append(line)
|
||||
with open(p,'w') as f: f.writelines(out)
|
||||
print('wzdj.sh updated')
|
||||
"
|
||||
else
|
||||
echo \"wzdj.sh not found, skip\"
|
||||
fi
|
||||
|
||||
echo "【3.5】释放 3055 端口(避免 EADDRINUSE)"
|
||||
fuser -k 3055/tcp 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
echo "【4】启动 wzdj"
|
||||
python3 -c "
|
||||
import hashlib,json,time,urllib.request,urllib.parse,ssl
|
||||
ssl._create_default_https_context=ssl._create_unverified_context
|
||||
P,K='https://127.0.0.1:9988','qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT'
|
||||
def sg():
|
||||
t=int(time.time())
|
||||
return {'request_time':t,'request_token':hashlib.md5((str(t)+hashlib.md5(K.encode()).hexdigest()).encode()).hexdigest()}
|
||||
def post(u,d=None):
|
||||
pl=sg()
|
||||
if d: pl.update(d)
|
||||
r=urllib.request.Request(P+u,data=urllib.parse.urlencode(pl).encode())
|
||||
return json.loads(urllib.request.urlopen(r,timeout=15).read().decode())
|
||||
r=post('/project/nodejs/start_project',{'project_name':'wzdj'})
|
||||
print('start wzdj:', r.get('msg') or r)
|
||||
"
|
||||
|
||||
echo "【5】完成"
|
||||
195
01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_修复wzdj启动.py
Normal file
195
01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_kr宝塔_修复wzdj启动.py
Normal file
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""TAT:修复 wzdj.quwanzhi.com 启动失败(Cannot find module '/www/wwwroot/self/wzdj')
|
||||
原因:宝塔用 node /path 当入口,应改为 cd /path && (pnpm start || npm run start)
|
||||
"""
|
||||
import base64, json, os, re, sys, time
|
||||
KR_INSTANCE_ID, REGION = "ins-aw0tnqjo", "ap-guangzhou"
|
||||
|
||||
SHELL = r'''#!/bin/bash
|
||||
set -e
|
||||
echo "【1】确保宝塔 9988 监听"
|
||||
if ! ss -tlnp 2>/dev/null | grep -q ':9988 '; then
|
||||
/etc/init.d/bt start 2>/dev/null || true
|
||||
sleep 8
|
||||
fi
|
||||
|
||||
echo "【2】停止 wzdj"
|
||||
python3 -c "
|
||||
import hashlib,json,time,urllib.request,urllib.parse,ssl
|
||||
ssl._create_default_https_context=ssl._create_unverified_context
|
||||
P,K='https://127.0.0.1:9988','qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT'
|
||||
def sg():
|
||||
t=int(time.time())
|
||||
return {'request_time':t,'request_token':hashlib.md5((str(t)+hashlib.md5(K.encode()).hexdigest()).encode()).hexdigest()}
|
||||
def post(u,d=None):
|
||||
pl=sg()
|
||||
if d: pl.update(d)
|
||||
r=urllib.request.Request(P+u,data=urllib.parse.urlencode(pl).encode())
|
||||
return json.loads(urllib.request.urlopen(r,timeout=15).read().decode())
|
||||
try:
|
||||
post('/project/nodejs/stop_project',{'project_name':'wzdj'})
|
||||
print('stop wzdj ok')
|
||||
except Exception as e: print('stop',e)
|
||||
" 2>/dev/null || true
|
||||
|
||||
echo "【3】修复 site.db 中 wzdj 的 project_script/run_cmd"
|
||||
python3 -c "
|
||||
import json,sqlite3,os
|
||||
db='/www/server/panel/data/db/site.db'
|
||||
path='/www/wwwroot/self/wzdj'
|
||||
cmd='cd %s && (PORT=3055 pnpm start 2>/dev/null || PORT=3055 npm run start)' % path
|
||||
conn=sqlite3.connect(db)
|
||||
c=conn.cursor()
|
||||
c.execute('SELECT id,name,path,project_config FROM sites WHERE name=\"wzdj\" AND project_type=\"Node\"')
|
||||
row=c.fetchone()
|
||||
if row:
|
||||
sid,nm,oldpath,cfg=row[0],row[1],row[2] or '',row[3] or '{}'
|
||||
try: cfg=json.loads(cfg)
|
||||
except: cfg={}
|
||||
cfg['project_script']=cfg['run_cmd']=cmd
|
||||
cfg['path']=path
|
||||
c.execute('UPDATE sites SET path=?, project_config=? WHERE id=?',(path,json.dumps(cfg,ensure_ascii=False),sid))
|
||||
conn.commit()
|
||||
print('site.db wzdj fixed:',cmd[:60])
|
||||
else:
|
||||
print('wzdj not found in sites')
|
||||
conn.close()
|
||||
"
|
||||
|
||||
echo "【4】修复 wzdj.sh 启动脚本(避免 node /path)"
|
||||
SH=/www/server/nodejs/vhost/scripts/wzdj.sh
|
||||
if [ -f \"$SH\" ]; then
|
||||
python3 -c "
|
||||
p='$SH'
|
||||
path='/www/wwwroot/self/wzdj'
|
||||
new_cmd='cd %s && (PORT=3055 pnpm start 2>/dev/null || PORT=3055 npm run start)' % path
|
||||
with open(p,'r') as f: lines=f.readlines()
|
||||
out=[]
|
||||
for line in lines:
|
||||
if '/www/wwwroot/self/wzdj' in line and not line.strip().startswith('#') and ('node' in line.lower() or 'exec' in line or '\$' in line):
|
||||
out.append(' ' + new_cmd + '\n')
|
||||
else:
|
||||
out.append(line)
|
||||
with open(p,'w') as f: f.writelines(out)
|
||||
print('wzdj.sh updated')
|
||||
"
|
||||
else
|
||||
echo \"wzdj.sh not found, skip\"
|
||||
fi
|
||||
|
||||
echo "【5】启动 wzdj"
|
||||
python3 -c "
|
||||
import hashlib,json,time,urllib.request,urllib.parse,ssl
|
||||
ssl._create_default_https_context=ssl._create_unverified_context
|
||||
P,K='https://127.0.0.1:9988','qcWubCdlfFjS2b2DMT1lzPFaDfmv1cBT'
|
||||
def sg():
|
||||
t=int(time.time())
|
||||
return {'request_time':t,'request_token':hashlib.md5((str(t)+hashlib.md5(K.encode()).hexdigest()).encode()).hexdigest()}
|
||||
def post(u,d=None):
|
||||
pl=sg()
|
||||
if d: pl.update(d)
|
||||
r=urllib.request.Request(P+u,data=urllib.parse.urlencode(pl).encode())
|
||||
return json.loads(urllib.request.urlopen(r,timeout=15).read().decode())
|
||||
r=post('/project/nodejs/start_project',{'project_name':'wzdj'})
|
||||
print('start wzdj:', r.get('msg') or r)
|
||||
" 2>&1
|
||||
|
||||
echo "【6】完成"
|
||||
'''
|
||||
|
||||
|
||||
def _creds():
|
||||
d = os.path.dirname(os.path.abspath(__file__))
|
||||
for _ in range(6):
|
||||
p = os.path.join(d, "运营中枢", "工作台", "00_账号与API索引.md")
|
||||
if os.path.isfile(p):
|
||||
t = open(p).read()
|
||||
sid = skey = None
|
||||
for L in t.splitlines():
|
||||
m = re.search(r"SecretId[^|]*\|\s*`([^`]+)`", L, re.I)
|
||||
if m and "AKID" in m.group(1): sid = m.group(1).strip()
|
||||
m = re.search(r"SecretKey\s*\|\s*`([^`]+)`", L, 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():
|
||||
base = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
||||
log_path = os.path.join(base, "运营中枢", "工作台", "wzdj_fix_result.txt")
|
||||
log_lines = []
|
||||
def log(s):
|
||||
log_lines.append(s)
|
||||
print(s)
|
||||
try:
|
||||
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||
with open(log_path, "w", encoding="utf-8") as f:
|
||||
f.write("start\n")
|
||||
except Exception:
|
||||
pass
|
||||
sid, skey = _creds()
|
||||
if not sid or not skey:
|
||||
log("❌ 未配置凭证")
|
||||
try:
|
||||
with open(log_path, "w", encoding="utf-8") as f:
|
||||
f.write("no creds\n")
|
||||
except Exception:
|
||||
pass
|
||||
return 1
|
||||
from tencentcloud.common import credential
|
||||
from tencentcloud.tat.v20201028 import tat_client, models
|
||||
cred = credential.Credential(sid, skey)
|
||||
cli = tat_client.TatClient(cred, REGION)
|
||||
req = models.RunCommandRequest()
|
||||
req.Content = base64.b64encode(SHELL.encode("utf-8")).decode()
|
||||
req.InstanceIds = [KR_INSTANCE_ID]
|
||||
req.CommandType = "SHELL"
|
||||
req.Timeout = 60
|
||||
req.CommandName = "kr宝塔_修复wzdj启动"
|
||||
r = cli.RunCommand(req)
|
||||
log("✅ TAT inv: " + str(r.InvocationId))
|
||||
time.sleep(25)
|
||||
req2 = models.DescribeInvocationTasksRequest()
|
||||
f = models.Filter()
|
||||
f.Name, f.Values = "invocation-id", [r.InvocationId]
|
||||
req2.Filters = [f]
|
||||
r2 = cli.DescribeInvocationTasks(req2)
|
||||
for t in (r2.InvocationTaskSet or []):
|
||||
log("状态: " + str(getattr(t, "TaskStatus", "")))
|
||||
tr = getattr(t, "TaskResult", None)
|
||||
if tr:
|
||||
d = tr.__dict__ if hasattr(tr, "__dict__") else {}
|
||||
out = d.get("Output", getattr(tr, "Output", ""))
|
||||
if out:
|
||||
try:
|
||||
decoded = base64.b64decode(out).decode("utf-8", errors="replace")
|
||||
log(decoded)
|
||||
except Exception:
|
||||
log(str(out)[:2000])
|
||||
try:
|
||||
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||
with open(log_path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(log_lines))
|
||||
except Exception:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
log_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))), "运营中枢", "工作台", "wzdj_fix_result.txt")
|
||||
code = 1
|
||||
try:
|
||||
code = main()
|
||||
with open(log_path, "w", encoding="utf-8") as f:
|
||||
f.write("OK exit=%s\n" % code)
|
||||
except Exception as e:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||
with open(log_path, "w", encoding="utf-8") as f:
|
||||
f.write("ERR: %s\n" % e)
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
sys.exit(code)
|
||||
@@ -28,7 +28,7 @@ def ports(it):
|
||||
p=[int(cfg['port'])] if cfg.get('port') else []
|
||||
p.extend(int(m) for m in re.findall(r'-p\s*(\d+)',str(cfg.get('project_script',''))))
|
||||
return sorted(set(p))
|
||||
FB={'玩值大屏':['/www/wwwroot/self/wanzhi/玩值大屏','/www/wwwroot/self/wanzhi/玩值'],'tongzhi':['/www/wwwroot/self/wanzhi/tongzhi','/www/wwwroot/self/wanzhi/tong'],'神射手':['/www/wwwroot/self/kr/kr-use','/www/wwwroot/self/kr/kr-users'],'AITOUFA':['/www/wwwroot/ext/tools/AITOUFA','/www/wwwroot/ext/tools/AITOL']}
|
||||
FB={'玩值大屏':['/www/wwwroot/self/wanzhi/玩值大屏','/www/wwwroot/self/wanzhi/玩值'],'tongzhi':['/www/wwwroot/self/wanzhi/tongzhi','/www/wwwroot/self/wanzhi/tong'],'神射手':['/www/wwwroot/self/kr/kr-use','/www/wwwroot/self/kr/kr-users'],'AITOUFA':['/www/wwwroot/ext/tools/AITOUFA','/www/wwwroot/ext/tools/AITOL'],'wzdj':['/www/wwwroot/wzdj']}
|
||||
db='/www/server/panel/data/db/site.db'
|
||||
if os.path.isfile(db):
|
||||
c=sqlite3.connect(db); cur=c.cursor(); cur.execute(\"SELECT id,name,path,project_config FROM sites WHERE project_type='Node'\")
|
||||
|
||||
@@ -78,6 +78,7 @@ PATH_FALLBACK = {
|
||||
"tongzhi": ["/www/wwwroot/self/wanzhi/tongzhi", "/www/wwwroot/self/wanzhi/tong"],
|
||||
"神射手": ["/www/wwwroot/self/kr/kr-use", "/www/wwwroot/self/kr/kr-users"],
|
||||
"AITOUFA": ["/www/wwwroot/ext/tools/AITOUFA", "/www/wwwroot/ext/tools/AITOL"],
|
||||
"wzdj": ["/www/wwwroot/wzdj"],
|
||||
}
|
||||
|
||||
# 【1】停止全部 Node
|
||||
|
||||
159
01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_wzdj_修正路径并重启.py
Normal file
159
01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_wzdj_修正路径并重启.py
Normal file
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
腾讯云 TAT:仅修正 wzdj 在 site.db 中的路径与启动命令并重启
|
||||
问题:宝塔里 wzdj 路径为 /www/wwwroot/self/wzdj 导致 MODULE_NOT_FOUND(Node 把路径当模块加载)
|
||||
处理:将 path 与 project_script 改为 /www/wwwroot/wzdj,再重启 wzdj
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
KR_INSTANCE_ID = "ins-aw0tnqjo"
|
||||
REGION = "ap-guangzhou"
|
||||
WZDJ_CORRECT_PATH = "/www/wwwroot/wzdj"
|
||||
DB_PATH = "/www/server/panel/data/db/site.db"
|
||||
|
||||
def _read_creds():
|
||||
d = os.path.dirname(os.path.abspath(__file__))
|
||||
for _ in range(6):
|
||||
root = d
|
||||
p = os.path.join(root, "运营中枢", "工作台", "00_账号与API索引.md")
|
||||
if os.path.isfile(p):
|
||||
with open(p, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
sid = skey = None
|
||||
in_tx = False
|
||||
for line in text.splitlines():
|
||||
if "### 腾讯云" in line:
|
||||
in_tx = True
|
||||
continue
|
||||
if in_tx and line.strip().startswith("###"):
|
||||
break
|
||||
if not in_tx:
|
||||
continue
|
||||
m = re.search(r"\|\s*[^|]*(?:SecretId|密钥)[^|]*\|\s*`([^`]+)`", line, re.I)
|
||||
if m and m.group(1).strip().startswith("AKID"):
|
||||
sid = m.group(1).strip()
|
||||
m = re.search(r"\|\s*SecretKey\s*\|\s*`([^`]+)`", line, re.I)
|
||||
if m:
|
||||
skey = m.group(1).strip()
|
||||
return sid or None, skey or None
|
||||
d = os.path.dirname(d)
|
||||
return None, None
|
||||
|
||||
SHELL = f'''#!/bin/bash
|
||||
set -e
|
||||
echo "=== 修正 wzdj 路径并重启 ==="
|
||||
db="{DB_PATH}"
|
||||
if [ ! -f "$db" ]; then
|
||||
echo "ERROR: site.db not found"; exit 1
|
||||
fi
|
||||
# 1. 修正 site.db 中 wzdj 的 path 与 project_config
|
||||
python3 -c "
|
||||
import json, sqlite3, os
|
||||
db = '{DB_PATH}'
|
||||
path = '{WZDJ_CORRECT_PATH}'
|
||||
conn = sqlite3.connect(db)
|
||||
c = conn.cursor()
|
||||
c.execute(\\\"SELECT id, name, path, project_config FROM sites WHERE project_type='Node' AND name='wzdj'\\\")
|
||||
row = c.fetchone()
|
||||
if not row:
|
||||
print('wzdj 未在 site.db 中找到')
|
||||
exit(1)
|
||||
sid, name, old_path, cfg_str = row
|
||||
cfg = json.loads(cfg_str or '{{}}')
|
||||
cmd = f'cd {{path}} && (pnpm start 2>/dev/null || npm run start)'
|
||||
cfg['project_script'] = cfg['run_cmd'] = cmd
|
||||
cfg['path'] = path
|
||||
c.execute('UPDATE sites SET path=?, project_config=? WHERE id=?', (path, json.dumps(cfg, ensure_ascii=False), sid))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print('已更新 wzdj path ->', path)
|
||||
"
|
||||
# 2. 重启 wzdj(宝塔 API)
|
||||
python3 -c "
|
||||
import hashlib, json, urllib.request, urllib.parse, ssl, time
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
P, 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(path, d=None):
|
||||
pl = sign()
|
||||
if d: pl.update(d)
|
||||
r = urllib.request.Request(P+path, data=urllib.parse.urlencode(pl).encode())
|
||||
with urllib.request.urlopen(r, timeout=25) 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:
|
||||
nm = (it.get('name') or '').lower()
|
||||
if nm == 'wzdj':
|
||||
post('/project/nodejs/restart_project', {{'project_name': it.get('name') or it.get('project_name')}})
|
||||
print(' 已重启 wzdj')
|
||||
break
|
||||
else:
|
||||
print(' 未找到 wzdj 项目')
|
||||
"
|
||||
echo "=== 完成 ==="
|
||||
'''
|
||||
|
||||
def main():
|
||||
sid = os.environ.get("TENCENTCLOUD_SECRET_ID")
|
||||
skey = os.environ.get("TENCENTCLOUD_SECRET_KEY")
|
||||
if not sid or not skey:
|
||||
sid, skey = _read_creds()
|
||||
if not sid or not skey:
|
||||
print("❌ 未配置腾讯云 SecretId/SecretKey(见 00_账号与API索引.md)")
|
||||
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.encode()).decode()
|
||||
req.InstanceIds = [KR_INSTANCE_ID]
|
||||
req.CommandType = "SHELL"
|
||||
req.Timeout = 90
|
||||
req.CommandName = "wzdj_fix_path_restart"
|
||||
resp = client.RunCommand(req)
|
||||
print("✅ TAT 已下发,InvocationId:", resp.InvocationId)
|
||||
print(" 修正 wzdj path 为", WZDJ_CORRECT_PATH, "并重启")
|
||||
print(" 等待约 55s...")
|
||||
time.sleep(55)
|
||||
try:
|
||||
req2 = models.DescribeInvocationTasksRequest()
|
||||
f = models.Filter()
|
||||
f.Name = "invocation-id"
|
||||
f.Values = [resp.InvocationId]
|
||||
req2.Filters = [f]
|
||||
r2 = client.DescribeInvocationTasks(req2)
|
||||
for t in (r2.InvocationTaskSet or []):
|
||||
print(" 状态:", getattr(t, "TaskStatus", ""))
|
||||
tr = getattr(t, "TaskResult", None)
|
||||
out = None
|
||||
if tr is not None:
|
||||
out = getattr(tr, "Output", tr.__dict__.get("Output", ""))
|
||||
if not out and hasattr(t, "Output"):
|
||||
out = t.Output
|
||||
if out:
|
||||
try:
|
||||
text = base64.b64decode(out).decode("utf-8", errors="replace")
|
||||
print(" 输出:", text[:1500])
|
||||
except Exception:
|
||||
print(" 输出:", (out or "")[:1000])
|
||||
except Exception as e:
|
||||
print(" 查询:", e)
|
||||
print(" 验证: https://wzdj.quwanzhi.com")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
93
01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_kr宝塔安全组放行SSH.py
Normal file
93
01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_kr宝塔安全组放行SSH.py
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
腾讯云 API 为 kr宝塔 43.139.27.93 安全组放行 SSH 端口 22、22022,便于远程连接。
|
||||
凭证:00_账号与API索引.md 或环境变量
|
||||
"""
|
||||
import os, re, sys
|
||||
KR_IP = "43.139.27.93"
|
||||
KR_INSTANCE_ID = "ins-aw0tnqjo"
|
||||
REGIONS = ["ap-guangzhou", "ap-beijing", "ap-shanghai"]
|
||||
|
||||
def _read_creds():
|
||||
d = os.path.dirname(os.path.abspath(__file__))
|
||||
for _ in range(6):
|
||||
if os.path.isdir(os.path.join(d, "运营中枢")) and os.path.isdir(os.path.join(d, "01_卡资(金)")):
|
||||
p = os.path.join(d, "运营中枢", "工作台", "00_账号与API索引.md")
|
||||
if os.path.isfile(p):
|
||||
with open(p) 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.cvm.v20170312 import cvm_client, models as cvm_models
|
||||
from tencentcloud.vpc.v20170312 import vpc_client, models as vpc_models
|
||||
except ImportError:
|
||||
print("pip install tencentcloud-sdk-python-cvm tencentcloud-sdk-python-vpc"); return 1
|
||||
cred = credential.Credential(sid, skey)
|
||||
sg_ids, region = [], None
|
||||
for r in REGIONS:
|
||||
try:
|
||||
c = cvm_client.CvmClient(cred, r)
|
||||
req = cvm_models.DescribeInstancesRequest()
|
||||
req.InstanceIds = [KR_INSTANCE_ID]
|
||||
resp = c.DescribeInstances(req)
|
||||
for ins in (getattr(resp, "InstanceSet", None) or []):
|
||||
sg_ids = list(getattr(ins, "SecurityGroupIds", None) or [])
|
||||
region = r
|
||||
break
|
||||
except Exception:
|
||||
try:
|
||||
req = cvm_models.DescribeInstancesRequest()
|
||||
req.Limit = 100
|
||||
resp = c.DescribeInstances(req)
|
||||
for ins in (getattr(resp, "InstanceSet", None) or []):
|
||||
if KR_IP in list(getattr(ins, "PublicIpAddresses", None) or []):
|
||||
sg_ids = list(getattr(ins, "SecurityGroupIds", None) or [])
|
||||
region = r
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if sg_ids: break
|
||||
if not sg_ids:
|
||||
print("❌ kr宝塔 %s 未在 CVM 中找到" % KR_IP); return 1
|
||||
print("kr宝塔 %s 安全组放行 SSH 22、22022" % KR_IP)
|
||||
vc = vpc_client.VpcClient(cred, region)
|
||||
for port, desc in [("22", "SSH"), ("22022", "SSH-宝塔")]:
|
||||
for sg_id in sg_ids:
|
||||
try:
|
||||
req = vpc_models.CreateSecurityGroupPoliciesRequest()
|
||||
req.SecurityGroupId = sg_id
|
||||
ps = vpc_models.SecurityGroupPolicySet()
|
||||
ing = vpc_models.SecurityGroupPolicy()
|
||||
ing.Protocol, ing.Port, ing.CidrBlock = "TCP", port, "0.0.0.0/0"
|
||||
ing.Action, ing.PolicyDescription = "ACCEPT", desc
|
||||
ps.Ingress = [ing]
|
||||
req.SecurityGroupPolicySet = ps
|
||||
vc.CreateSecurityGroupPolicies(req)
|
||||
print(" ✅ %s 已添加 %s/TCP" % (sg_id, port))
|
||||
except Exception as e:
|
||||
if "RuleAlreadyExists" in str(e) or "已存在" in str(e) or "duplicate" in str(e).lower():
|
||||
print(" ⏭ %s %s 规则已存在" % (sg_id, port))
|
||||
else:
|
||||
print(" ❌ %s %s: %s" % (sg_id, port, e))
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -688,6 +688,18 @@ ssh nas "netstat -tlnp | grep 27017"
|
||||
| DSM | http://192.168.110.29:5000 |
|
||||
| 账号 | admin(密码见上,小写) |
|
||||
|
||||
### 家里 NAS:开辟约 1TB 备份盘 · Mac 当硬盘用 + 时间机器
|
||||
|
||||
**需求**:在群晖(家里 DiskStation)上开辟约 1000GB 空间,在 Mac 上挂载成**像真实硬盘**,可用于时间机器 + 日常读写。
|
||||
|
||||
- **操作指南**:`参考资料/群晖1TB备份盘_Mac挂载与时间机器.md`(NAS 新建共享 → 开 Time Machine → Mac 挂载 → 时间机器选盘 → 可选开机自动挂载)
|
||||
- **一键挂载**(内网优先,外网走 frp 4452):
|
||||
```bash
|
||||
/Users/karuo/Documents/个人/卡若AI/01_卡资(金)/金仓_存储备份/群晖NAS管理/scripts/mount_diskstation_1tb.sh
|
||||
```
|
||||
- 挂载点:`~/DiskStation-1TB`;新建共享名若为 `MacBackup`,可执行:`MACBACKUP_SHARE=MacBackup ./scripts/mount_diskstation_1tb.sh`
|
||||
- **开机自动挂载**:复制 `scripts/com.karuo.mount_diskstation_1tb.plist` 到 `~/Library/LaunchAgents/` 后 `launchctl load`,详见操作指南。
|
||||
|
||||
---
|
||||
|
||||
## 运维规范
|
||||
@@ -718,6 +730,7 @@ ssh nas "netstat -tlnp | grep 27017"
|
||||
| 脚本 | 功能 | 位置 | 快速运行 |
|
||||
|------|------|------|----------|
|
||||
| `time_machine_diskstation_auto.sh` | Time Machine → 家里 DiskStation 检测/验证,输出材料路径供按参考资料处理 | `./scripts/` | `./scripts/time_machine_diskstation_auto.sh` |
|
||||
| `mount_diskstation_1tb.sh` | 家里 NAS 约 1TB 备份盘挂载到 Mac(内网优先),当硬盘用 + 时间机器 | `./scripts/` | `./scripts/mount_diskstation_1tb.sh` |
|
||||
| `export_macos_vm_to_downloads.sh` | CKB NAS 上 macOS VM 打包下载到本机「下载」文件夹,实时显示大小与用时,并生成使用说明 | `./scripts/` | 见下方「macOS VM 导出到本机」 |
|
||||
| `optimize_macos_vm_compose.sh` | 本机→NAS:macOS VM 流畅度优化 | `./scripts/` | 需本机与 NAS 同网 |
|
||||
| `optimize_macos_vm_on_nas.sh` | **NAS 上直接执行**:macOS VM 流畅度优化(外网推荐) | `./scripts/` | SSH 登录 NAS 后运行 |
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.karuo.mount_diskstation_1tb</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>/Users/karuo/Documents/个人/卡若AI/01_卡资(金)/金仓_存储备份/群晖NAS管理/scripts/mount_diskstation_1tb.sh</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/mount_diskstation_1tb.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/mount_diskstation_1tb.err</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,19 +1,19 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# 家里 DiskStation 1TB 共享 - 外网挂载到 Finder 侧栏「位置」
|
||||
# 挂载后可直接存文件,Finder 拷贝时会显示速率
|
||||
# 外网通过 frp 端口 4452 访问 SMB(需先在 NAS 添加 frpc 配置)
|
||||
# 家里 DiskStation 约 1TB 备份盘 - Mac 挂载成像真实硬盘
|
||||
# 内网优先(192.168.110.29),外网用 opennas2:4452
|
||||
# 挂载后可用于:时间机器、Finder 侧栏当硬盘用、拷贝看速率
|
||||
# ============================================
|
||||
|
||||
NAS_HOST="opennas2.quwanzhi.com"
|
||||
NAS_PORT="4452"
|
||||
NAS_USER="admin"
|
||||
# 密码:与 DSM 登录一致
|
||||
NAS_PASS="zhiqun1984"
|
||||
# 共享名:DSM 中的共享文件夹名,常见为 共享、homes
|
||||
SHARE="共享"
|
||||
# 共享名:DSM 里新建的备份盘可用 MacBackup,已有共享可用 共享
|
||||
SHARE_RAW="${MACBACKUP_SHARE:-共享}"
|
||||
MOUNT_POINT="$HOME/DiskStation-1TB"
|
||||
|
||||
# 中文共享名需 URL 编码,否则 mount_smbfs 会报 URL parsing failed
|
||||
SHARE=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$SHARE_RAW'))" 2>/dev/null || echo "$SHARE_RAW")
|
||||
|
||||
# 已挂载则先卸载
|
||||
if mount | grep -q "DiskStation-1TB"; then
|
||||
echo "正在卸载旧挂载..."
|
||||
@@ -22,15 +22,27 @@ if mount | grep -q "DiskStation-1TB"; then
|
||||
fi
|
||||
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
echo "正在挂载家里 DiskStation (${NAS_HOST}:${NAS_PORT})..."
|
||||
mount_smbfs "//${NAS_USER}:${NAS_PASS}@${NAS_HOST}:${NAS_PORT}/${SHARE}" "$MOUNT_POINT" 2>&1
|
||||
|
||||
# 内网优先:能 ping 通 192.168.110.29 则用内网(SMB 默认 445)
|
||||
if ping -c 1 -W 2 192.168.110.29 >/dev/null 2>&1; then
|
||||
echo "使用内网 192.168.110.29 挂载..."
|
||||
SMB_URL="//${NAS_USER}:${NAS_PASS}@192.168.110.29/${SHARE}"
|
||||
else
|
||||
echo "使用外网 opennas2.quwanzhi.com:4452 挂载..."
|
||||
SMB_URL="//${NAS_USER}:${NAS_PASS}@opennas2.quwanzhi.com:4452/${SHARE}"
|
||||
fi
|
||||
|
||||
mount_smbfs "$SMB_URL" "$MOUNT_POINT" 2>&1
|
||||
|
||||
if mount | grep -q "DiskStation-1TB"; then
|
||||
echo "挂载成功: $MOUNT_POINT"
|
||||
echo "添加到 Finder 侧栏:在 Finder 中把「DiskStation-1TB」拖到侧栏「位置」下即可"
|
||||
echo "直接往里拷贝文件,Finder 会显示传输速率"
|
||||
echo "→ 时间机器:系统设置 → 时间机器 → 选择该磁盘"
|
||||
echo "→ 侧栏固定:在 Finder 中把「DiskStation-1TB」拖到「位置」"
|
||||
open "$MOUNT_POINT"
|
||||
else
|
||||
echo "挂载失败。请确认:1) 家里 NAS frpc 已添加 SMB 4452 端口 2) NAS_PASS 正确 3) 共享名为 ${SHARE}"
|
||||
echo "挂载失败。请确认:"
|
||||
echo " 1) NAS 上已建共享文件夹(如 ${SHARE_RAW}),SMB 已开"
|
||||
echo " 2) 外网时 frpc 已添加 SMB 4452"
|
||||
echo " 3) 密码正确(可改本脚本 NAS_PASS)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
138
01_卡资(金)/金仓_存储备份/群晖NAS管理/参考资料/群晖1TB备份盘_Mac挂载与时间机器.md
Normal file
138
01_卡资(金)/金仓_存储备份/群晖NAS管理/参考资料/群晖1TB备份盘_Mac挂载与时间机器.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# 群晖 NAS 开辟 1TB 备份盘 · Mac 当硬盘用 + 时间机器
|
||||
|
||||
在群晖(家里 DiskStation)上开辟约 **1000GB** 空间,在 Mac 上挂载成**像真实硬盘一样**使用,并可用于**时间机器**备份。
|
||||
|
||||
---
|
||||
|
||||
## 一、NAS 端:开辟 1TB 空间(DSM 操作)
|
||||
|
||||
在浏览器打开 **http://192.168.110.29:5000** 登录 DSM,按顺序做:
|
||||
|
||||
### 1. 新建共享文件夹(若还没有专用备份盘)
|
||||
|
||||
1. **文件服务** → **共享文件夹** → **新增**
|
||||
2. 名称:`MacBackup`(或 `备份盘`,英文更省事)
|
||||
3. 位置:选 **volume1**
|
||||
4. 容量:**不勾选「启用共享文件夹配额」** 即使用整个卷可用空间;若需限制为 1TB,勾选并设 **1024 GB**
|
||||
5. 权限:给 **admin** 读写
|
||||
6. 高级:**关闭回收站**(时间机器兼容更好)
|
||||
7. 完成
|
||||
|
||||
若已有「共享」且空间够 1TB,可跳过新建,直接用该共享;下面以 **共享名 = MacBackup** 为例(你用「共享」则替换成 `共享`)。
|
||||
|
||||
### 2. 开启 Time Machine 支持(可选,用于时间机器)
|
||||
|
||||
1. **控制面板** → **文件服务** → **高级** 或 **Time Machine**
|
||||
2. 勾选 **「启用 Time Machine 备份」**
|
||||
3. 选择刚建的 **MacBackup**(或你用的共享名)
|
||||
4. 保存
|
||||
|
||||
### 3. 确认 SMB 已启用
|
||||
|
||||
1. **控制面板** → **文件服务** → **SMB**
|
||||
2. 勾选 **启用 SMB 服务**
|
||||
3. 高级里建议:**最大 SMB 协议 = SMB3**,勾选 **SMB2 租约**、**持久句柄**
|
||||
4. 保存
|
||||
|
||||
### 4. 外网访问(可选):frpc 添加 SMB 端口
|
||||
|
||||
不在家时也要挂载,需在 NAS 上给 frpc 加 SMB 穿透:
|
||||
|
||||
- SSH 登录:`ssh admin@192.168.110.29`(需内网或已配好 SSH)
|
||||
- 编辑:`/volume1/homes/admin/frpc/frpc.ini`,在末尾加:
|
||||
|
||||
```ini
|
||||
# SMB(外网 4452 → NAS 445)
|
||||
[home-nas-smb]
|
||||
type = tcp
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 445
|
||||
remote_port = 4452
|
||||
```
|
||||
|
||||
- 重启 frpc:`/volume1/homes/admin/frpc/start_frpc.sh`
|
||||
|
||||
---
|
||||
|
||||
## 二、Mac 端:挂载成像真实硬盘
|
||||
|
||||
### 方式 A:内网挂载(推荐,在家时)
|
||||
|
||||
1. Finder → **前往** → **连接服务器**(⌘K)
|
||||
2. 输入(把 `MacBackup` 换成你的共享名):
|
||||
- **`smb://192.168.110.29/MacBackup`**
|
||||
3. 连接,输入 DSM 账号密码,勾选「在钥匙串中记住」
|
||||
4. 挂载后会在 Finder 侧栏「位置」出现,**像本地硬盘一样**拖拽、拷贝、时间机器选它即可
|
||||
|
||||
### 方式 B:外网挂载(不在家时)
|
||||
|
||||
1. 确保 NAS 已按「一、4」添加 SMB 的 frpc
|
||||
2. Finder → ⌘K,输入:
|
||||
- **`smb://opennas2.quwanzhi.com:4452/MacBackup`**
|
||||
3. 连接并记住密码,同样当硬盘用
|
||||
|
||||
### 方式 C:脚本一键挂载(内网优先)
|
||||
|
||||
已为你准备好脚本,**内网自动用 192.168.110.29,外网用 opennas2:4452**:
|
||||
|
||||
```bash
|
||||
/Users/karuo/Documents/个人/卡若AI/01_卡资(金)/金仓_存储备份/群晖NAS管理/scripts/mount_diskstation_1tb.sh
|
||||
```
|
||||
|
||||
- 挂载点:**~/DiskStation-1TB**
|
||||
- 在 Finder 里把 **DiskStation-1TB** 拖到侧栏「位置」,就像本机硬盘一样常驻
|
||||
|
||||
---
|
||||
|
||||
## 三、用作时间机器备份盘
|
||||
|
||||
1. **系统设置** → **通用** → **时间机器**
|
||||
2. 点 **「选择备份磁盘」** 或 **「+」**
|
||||
3. 选择刚挂载的 **MacBackup** 或 **DiskStation-1TB**
|
||||
4. 输入 DSM 账号密码(若提示)
|
||||
5. 若提示「未识别备份磁盘」:先移除该目标,再重新添加一次同一共享即可
|
||||
|
||||
时间机器会像用外接硬盘一样使用这块约 1TB 空间。
|
||||
|
||||
---
|
||||
|
||||
## 四、开机自动挂载(像真实硬盘常驻)
|
||||
|
||||
让 Mac 每次开机自动挂载,无需每次手动 ⌘K:
|
||||
|
||||
### 方法 1:登录项 + 脚本(简单)
|
||||
|
||||
1. 打开 **系统设置** → **通用** → **登录项**
|
||||
2. 点 **「+」**,选 **「其他」**
|
||||
3. 找到并选中脚本:
|
||||
`卡若AI/01_卡资(金)/金仓_存储备份/群晖NAS管理/scripts/mount_diskstation_1tb.sh`
|
||||
4. 之后每次登录会自动执行脚本挂载(需家里网络或外网 frp 可用)
|
||||
|
||||
### 方法 2:LaunchAgent(后台静默)
|
||||
|
||||
脚本同目录下已有 **`com.karuo.mount_diskstation_1tb.plist`**。安装步骤:
|
||||
|
||||
```bash
|
||||
# 复制到用户级 LaunchAgents
|
||||
cp "/Users/karuo/Documents/个人/卡若AI/01_卡资(金)/金仓_存储备份/群晖NAS管理/scripts/com.karuo.mount_diskstation_1tb.plist" ~/Library/LaunchAgents/
|
||||
|
||||
# 加载,下次登录会自动挂载
|
||||
launchctl load ~/Library/LaunchAgents/com.karuo.mount_diskstation_1tb.plist
|
||||
```
|
||||
|
||||
卸载:`launchctl unload ~/Library/LaunchAgents/com.karuo.mount_diskstation_1tb.plist`
|
||||
|
||||
---
|
||||
|
||||
## 五、小结
|
||||
|
||||
| 步骤 | 位置 | 动作 |
|
||||
|:-----|:-----|:-----|
|
||||
| 1 | NAS DSM | 新建共享文件夹(如 MacBackup)约 1TB,关回收站,开 SMB |
|
||||
| 2 | NAS DSM | 启用 Time Machine 并选中该共享 |
|
||||
| 3 | NAS(可选) | frpc 添加 SMB 4452,外网可挂载 |
|
||||
| 4 | Mac | ⌘K 连 `smb://192.168.110.29/MacBackup` 或运行挂载脚本 |
|
||||
| 5 | Mac | 时间机器里选该卷;侧栏固定后当真实硬盘用 |
|
||||
| 6 | Mac(可选) | 登录项加脚本,开机自动挂载 |
|
||||
|
||||
按上述做完后,你这台苹果电脑就可以把群晖上的约 1TB 当**一块真实硬盘**使用,并用于时间机器备份。
|
||||
28
02_卡人(水)/水溪_整理归档/经验库/待沉淀/wzdj_宝塔路径错误_MODULE_NOT_FOUND_修复.md
Normal file
28
02_卡人(水)/水溪_整理归档/经验库/待沉淀/wzdj_宝塔路径错误_MODULE_NOT_FOUND_修复.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# wzdj 启动失败 MODULE_NOT_FOUND 修复经验
|
||||
|
||||
> 来源:卡若AI 服务器管理 | 2026-03-03
|
||||
|
||||
## 现象
|
||||
|
||||
- 宝塔 Node 项目 **wzdj** 启动失败,弹窗报错:`Error: Cannot find module '/www/wwwroot/self/wzdj'`(MODULE_NOT_FOUND)
|
||||
- 站点 https://wzdj.quwanzhi.com 无法打开(超时或 502)
|
||||
|
||||
## 根因
|
||||
|
||||
- 宝塔里 wzdj 的**项目路径**配置为 `/www/wwwroot/self/wzdj`,而实际项目部署在 **`/www/wwwroot/wzdj`**(无 self 子目录)。
|
||||
- 启动命令错误时,Node 会把「路径」当作模块入口加载,导致 MODULE_NOT_FOUND。
|
||||
|
||||
## 处理
|
||||
|
||||
1. **仅修正路径并重启**(推荐)
|
||||
执行:`01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_TAT_wzdj_修正路径并重启.py`
|
||||
- 在 kr宝塔 上把 site.db 中 wzdj 的 `path` 改为 `/www/wwwroot/wzdj`,`project_script` 改为 `cd /www/wwwroot/wzdj && (pnpm start 2>/dev/null || npm run start)`,并调用宝塔 API 重启 wzdj。
|
||||
|
||||
2. **若仍无法访问**:在 kr宝塔 上确认 `/www/wwwroot/wzdj` 下有 `package.json`、已执行过 `pnpm build`,再执行一次「拉取构建并重启」:
|
||||
`scripts/腾讯云_TAT_wzdj_拉取构建并重启.py`
|
||||
|
||||
3. **批量修复时兜底**:`腾讯云_TAT_kr宝塔_强制停启Node.py` 与 `腾讯云_TAT_kr宝塔_运行堵塞与Node深度修复.py` 的 PATH_FALLBACK 已加入 `"wzdj": ["/www/wwwroot/wzdj"]`,路径错误时会被自动纠正。
|
||||
|
||||
## 可复用
|
||||
|
||||
- 同类问题:宝塔 Node 项目报 MODULE_NOT_FOUND 且错误里是「路径」→ 先查 site.db 里该项目 path 是否指向真实项目目录,再改 path + project_script(`cd 正确路径 && (pnpm start || npm run start)`)并重启。
|
||||
@@ -46,11 +46,14 @@
|
||||
|
||||
## 第五步:加封面 + 烧录字幕 → 成片
|
||||
|
||||
1. **封面**:前 3 秒使用高光时刻/提问文案,半透明质感,Soul 绿风格。
|
||||
2. **字幕**:烧录到画面,封面结束后显示,居中去语助词。
|
||||
3. **输出**:竖屏 498×1080(可选)、文件名为纯标题,全部写入 **成片/**。
|
||||
成片**必做**四项,且执行时有进度输出(【成片进度】1/6 …、[1/5] 封面 [2/5] 字幕 [3/5] 加速 [4/5] 裁剪 [5/5] 完成):
|
||||
|
||||
**产出**:**成片/** 目录下的成片 mp4(封面+字幕+去语助词)。
|
||||
1. **封面**:前 3 秒使用高光时刻/提问文案,半透明质感,Soul 绿风格。
|
||||
2. **字幕**:烧录到画面、**随语音时间轴走动**(非单张图),封面结束后显示,居中去语助词。若转录稿异常(如整篇同一句),会自动跳过字幕并提示「请用 MLX Whisper 重新生成 transcript.srt」。
|
||||
3. **加速 10%**:成片统一加速,节奏更紧凑。
|
||||
4. **竖屏裁剪**:高光区域裁成 498×1080 直出。
|
||||
|
||||
**产出**:**成片/** 目录下的成片 mp4(封面+字幕+加速+竖屏)。
|
||||
|
||||
---
|
||||
|
||||
@@ -68,6 +71,9 @@
|
||||
|
||||
**命名与标题统一**:成片文件名 = 封面显示标题 = `highlights.json` 的 `title`;对 title 做「去杠」(`:|、—、/` 等替换为空格),保证无序号、无多余符号,名字与标题一致。
|
||||
|
||||
**切片标题要求**:每条切片的 `title` 要写清楚、与时间段内容一致,且尽量用**热门观点**式表达(一句说清话题、有观点或悬念),便于封面与文件名统一且好懂。
|
||||
**字幕烧录前提**:烧录依赖正确的 `transcript.srt`(须为该视频的 MLX Whisper 等转录结果);若转录稿错误或全篇重复句,会无有效字幕或烧录内容错误,需重新转录后再跑成片。
|
||||
|
||||
---
|
||||
|
||||
## 本地处理说明(与剪映逆向分析一致)
|
||||
|
||||
@@ -278,6 +278,21 @@ def _filter_relevant_subtitles(subtitles):
|
||||
return out
|
||||
|
||||
|
||||
def _is_bad_transcript(subtitles, min_lines=15, max_repeat_ratio=0.85):
|
||||
"""检测是否为异常转录(如整篇同一句话):若大量重复则视为无效,不烧录错误字幕"""
|
||||
if not subtitles or len(subtitles) < min_lines:
|
||||
return False
|
||||
from collections import Counter
|
||||
texts = [ (s.get("text") or "").strip() for s in subtitles ]
|
||||
most_common = Counter(texts).most_common(1)
|
||||
if not most_common:
|
||||
return False
|
||||
_, count = most_common[0]
|
||||
if count >= len(texts) * max_repeat_ratio:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _sec_to_srt_time(sec):
|
||||
"""秒数转为 SRT 时间格式 HH:MM:SS,mmm"""
|
||||
h = int(sec) // 3600
|
||||
@@ -739,7 +754,7 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
|
||||
force_burn_subs=False, skip_subs=False, vertical=False):
|
||||
"""增强单个切片。vertical=True 时最后裁成竖屏 498x1080 直出成片。"""
|
||||
|
||||
print(f"\n处理: {os.path.basename(clip_path)}")
|
||||
print(f" 输入: {os.path.basename(clip_path)}", flush=True)
|
||||
|
||||
video_info = get_video_info(clip_path)
|
||||
width, height = video_info['width'], video_info['height']
|
||||
@@ -761,9 +776,10 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
|
||||
overlay_pos = f"{OVERLAY_X}:0" if vertical else "0:0"
|
||||
|
||||
# 1. 生成封面
|
||||
print(f" [1/5] 封面生成中…", flush=True)
|
||||
cover_img = os.path.join(temp_dir, 'cover.png')
|
||||
create_cover_image(hook_text, out_w, out_h, cover_img, clip_path)
|
||||
print(f" ✓ 封面生成")
|
||||
print(f" ✓ 封面生成", flush=True)
|
||||
|
||||
# 2. 字幕逻辑:有字幕则烧录(图像 overlay:每张图 -loop 1 才能按时间 enable 显示)
|
||||
sub_images = []
|
||||
@@ -789,13 +805,19 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
|
||||
sub['text'] = _translate_to_chinese(sub['text']) or sub['text']
|
||||
# 仅过滤整句为规则/模板的条目,保留所有对白(含重复句,保证字幕连续)
|
||||
subtitles = _filter_relevant_subtitles(subtitles)
|
||||
print(f" ✓ 字幕解析 ({len(subtitles)}条)")
|
||||
# 异常转录检测:若整篇几乎同一句话,不烧录错误字幕,避免成片出现“像图片”的无效字
|
||||
if _is_bad_transcript(subtitles):
|
||||
print(f" ⚠ 转录稿异常(大量重复同一句),已跳过字幕烧录;请用 MLX Whisper 对该视频重新生成 transcript.srt 后再跑成片", flush=True)
|
||||
sys.stdout.flush()
|
||||
subtitles = []
|
||||
else:
|
||||
print(f" ✓ 字幕解析 ({len(subtitles)}条),将烧录为随语音走动的字幕", flush=True)
|
||||
for i, sub in enumerate(subtitles):
|
||||
img_path = os.path.join(temp_dir, f'sub_{i:04d}.png')
|
||||
create_subtitle_image(sub['text'], out_w, out_h, img_path)
|
||||
sub_images.append({'path': img_path, 'start': sub['start'], 'end': sub['end']})
|
||||
if sub_images:
|
||||
print(f" ✓ 字幕图片 ({len(sub_images)}张)")
|
||||
print(f" ✓ 字幕图片 ({len(sub_images)}张)", flush=True)
|
||||
|
||||
# 4. 检测静音
|
||||
silences = detect_silence(clip_path, SILENCE_THRESHOLD, SILENCE_MIN_DURATION)
|
||||
@@ -804,13 +826,14 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
|
||||
# 5. 构建FFmpeg命令
|
||||
current_video = clip_path
|
||||
|
||||
# 5.1 添加封面(竖屏时叠在 x=543,与最终裁切区域对齐)
|
||||
# 5.1 添加封面(封面图 -loop 1 保证前 3 秒完整显示;竖屏时叠在 x=543)
|
||||
print(f" [2/5] 封面烧录中…", flush=True)
|
||||
cover_output = os.path.join(temp_dir, 'with_cover.mp4')
|
||||
cmd = [
|
||||
'ffmpeg', '-y',
|
||||
'-i', current_video, '-i', cover_img,
|
||||
'ffmpeg', '-y', '-i', current_video,
|
||||
'-loop', '1', '-i', cover_img,
|
||||
'-filter_complex', f"[0:v][1:v]overlay={overlay_pos}:enable='lt(t,{cover_duration})'[v]",
|
||||
'-map', '[v]', '-map', '0:a',
|
||||
'-map', '[v]', '-map', '0:a', '-shortest',
|
||||
'-c:v', 'libx264', '-preset', 'fast', '-crf', '22',
|
||||
'-c:a', 'copy', cover_output
|
||||
]
|
||||
@@ -818,11 +841,13 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
|
||||
|
||||
if os.path.exists(cover_output):
|
||||
current_video = cover_output
|
||||
print(f" ✓ 封面烧录")
|
||||
print(f" ✓ 封面烧录", flush=True)
|
||||
|
||||
# 5.2 烧录字幕(图像 overlay;每张图 -loop 1 才能按 enable=between(t,a,b) 显示)
|
||||
# 5.2 烧录字幕(图像 overlay;每张图 -loop 1 才能按 enable=between(t,a,b) 显示,随语音走动)
|
||||
if sub_images:
|
||||
print(f" [3/5] 字幕烧录中({len(sub_images)} 条,随语音时间轴显示)…", flush=True)
|
||||
batch_size = 5
|
||||
total_batches = (len(sub_images) + batch_size - 1) // batch_size
|
||||
for batch_idx in range(0, len(sub_images), batch_size):
|
||||
batch = sub_images[batch_idx:batch_idx + batch_size]
|
||||
inputs = ['-i', current_video]
|
||||
@@ -854,9 +879,18 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
|
||||
print(f" ⚠ 字幕批次 {batch_idx} 报错: {(result.stderr or '')[-500:]}", file=sys.stderr)
|
||||
if result.returncode == 0 and os.path.exists(batch_output):
|
||||
current_video = batch_output
|
||||
print(f" ✓ 字幕烧录")
|
||||
cur_batch = batch_idx // batch_size + 1
|
||||
if total_batches > 1 and cur_batch <= total_batches:
|
||||
print(f" 字幕批次 {cur_batch}/{total_batches} 完成", flush=True)
|
||||
if sub_images:
|
||||
print(f" ✓ 字幕烧录完成 ({len(sub_images)} 条)", flush=True)
|
||||
else:
|
||||
if do_burn_subs and os.path.exists(transcript_path):
|
||||
print(f" ⚠ 未烧录字幕:解析后无有效字幕(请用 MLX Whisper 重新生成 transcript.srt)", flush=True)
|
||||
print(f" [3/5] 字幕跳过", flush=True)
|
||||
|
||||
# 5.3 加速10% + 音频同步
|
||||
# 5.3 加速10% + 音频同步(成片必做)
|
||||
print(f" [4/5] 加速 10% + 去语助词(已在上步字幕解析中清理)…", flush=True)
|
||||
speed_output = os.path.join(temp_dir, 'speed.mp4')
|
||||
atempo = 1.0 / SPEED_FACTOR # 音频需要反向调整
|
||||
|
||||
@@ -869,22 +903,31 @@ def enhance_clip(clip_path, output_path, highlight_info, temp_dir, transcript_pa
|
||||
speed_output
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode == 0 and os.path.exists(speed_output):
|
||||
current_video = speed_output
|
||||
print(f" ✓ 加速10%")
|
||||
print(f" ✓ 加速 10% 完成", flush=True)
|
||||
else:
|
||||
print(f" ⚠ 加速步骤失败,沿用当前视频继续", file=sys.stderr)
|
||||
if result.stderr:
|
||||
print(f" {str(result.stderr)[:300]}", file=sys.stderr)
|
||||
|
||||
# 5.4 输出:竖屏则裁成 498x1080 直出,否则直接复制
|
||||
# 5.4 输出:竖屏则裁成 498x1080 直出(高光区域裁剪,成片必做)
|
||||
print(f" [5/5] 竖屏裁剪中(498×1080)…", flush=True)
|
||||
if vertical:
|
||||
r = subprocess.run([
|
||||
'ffmpeg', '-y', '-i', current_video,
|
||||
'-vf', CROP_VF, '-c:a', 'copy', output_path
|
||||
], capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
print(f" ❌ 竖屏裁剪失败: {r.stderr[:200]}", file=sys.stderr)
|
||||
if r.returncode == 0 and os.path.exists(output_path):
|
||||
print(f" ✓ 竖屏裁剪完成", flush=True)
|
||||
else:
|
||||
print(f" ❌ 竖屏裁剪失败: {(r.stderr or '')[:300]}", file=sys.stderr)
|
||||
shutil.copy(current_video, output_path)
|
||||
print(f" ⚠ 已回退为未裁剪版本,请检查 FFmpeg", flush=True)
|
||||
else:
|
||||
shutil.copy(current_video, output_path)
|
||||
print(f" ✓ 横版输出", flush=True)
|
||||
|
||||
if os.path.exists(output_path):
|
||||
size_mb = os.path.getsize(output_path) / (1024 * 1024)
|
||||
@@ -941,12 +984,17 @@ def main():
|
||||
highlights = highlights if isinstance(highlights, list) else []
|
||||
|
||||
clips = sorted(clips_dir.glob('*.mp4'))
|
||||
print(f"\n找到 {len(clips)} 个切片")
|
||||
total = len(clips)
|
||||
print(f"\n找到 {total} 个切片,开始成片(封面+字幕+加速10%+竖屏裁剪)\n", flush=True)
|
||||
|
||||
success_count = 0
|
||||
for i, clip_path in enumerate(clips):
|
||||
clip_num = _parse_clip_index(clip_path.name) or (i + 1)
|
||||
highlight_info = highlights[clip_num - 1] if 0 < clip_num <= len(highlights) else {}
|
||||
title_display = (highlight_info.get('title') or clip_path.stem)[:36]
|
||||
print("=" * 60, flush=True)
|
||||
print(f"【成片进度】 {i+1}/{total} {title_display}", flush=True)
|
||||
print("=" * 60, flush=True)
|
||||
|
||||
if getattr(args, 'title_only', False):
|
||||
title = (highlight_info.get('title') or highlight_info.get('hook_3sec') or clip_path.stem)
|
||||
|
||||
44
BOOTSTRAP.md
44
BOOTSTRAP.md
@@ -28,7 +28,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 三、启动顺序(Boot Sequence)
|
||||
## 三、启动顺序与运转流程(Boot Sequence)
|
||||
|
||||
每次新对话开始,按以下顺序加载上下文(**只读必要的,不要全读**):
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
| 1 | **本文件** `BOOTSTRAP.md` | 知道自己是谁、团队怎么分、怎么工作 |
|
||||
| 2 | **技能注册表** `SKILL_REGISTRY.md` | 查找用户需求对应的技能和路径 |
|
||||
| 3 | **对应技能的 SKILL.md** | 拿到具体执行指令(只读匹配到的那个) |
|
||||
| 4 | **(强制)在对话中展示思考与拆解(文字版)** | 接到用户任务并完成理解后,**必须先在本轮对话中以详细文字输出**:① 思考结果 ② 任务拆解 ③ 执行计划;**每次对话均须展示,不可跳过**;展示完毕后再进入执行。 |
|
||||
|
||||
**运转流程强制一环**:了解完用户需求后 → **深度思考与拆解** → **在对话里用详细文字展示**(思考结果 + 任务拆解 + 计划)→ 再继续往下执行。此步为强制,不可省略。
|
||||
|
||||
**启动瘦身(按需加载)**:步骤 2 不需要全表扫描。优先只加载 `SKILL_REGISTRY.md` 中 🔴 热技能(≤8 个)的触发词+路径;未命中时再懒加载其余部分。详见 SKILL_REGISTRY 中「技能热度分级」。
|
||||
|
||||
@@ -60,30 +63,51 @@
|
||||
|
||||
---
|
||||
|
||||
## 四.1、并行处理(多线程 · 一次对话内 1~6 线程)
|
||||
|
||||
**当任务可拆为多个相对独立的子任务时**,卡若AI 应启用**多线程/多子任务并行处理**,在一次对话内同时推进多条线,发挥全部能力。
|
||||
|
||||
- **数量**:可开 **1~6 个**并行线程(子任务)。按任务复杂度与独立性决定:单一线索用 1 个;多域、多技能、可独立推进的拆成 2~6 个同时处理。
|
||||
- **边界与域**:卡若AI 负责**规范各线程的边界与归属域**,避免重叠与冲突:
|
||||
- 按**五行/成员**划分:金(存储/安全)、水(整理/规划/对接)、木(内容/逆向/模板)、火(全栈/修复/追问/知识)、土(商业/技能/流量/财务)。
|
||||
- 按**技能域**划分:每个子任务对应明确 SKILL 或子技能,边界内全力处理。
|
||||
- **执行要求**:各线程在各自边界内**独立判断、独立处理**;能理解、能判断、能处理的事情在该线程内**全发挥**,不等待主线程逐项派单。主控只负责拆解、划界、派发与汇总。
|
||||
- **汇总**:所有并行线程完成后,由卡若AI 汇总结果、去重、合并结论,再进入验证与复盘。
|
||||
- **平台差异**:支持并行派发的平台(如 Cursor 的 mcp_task 等)可一次派发 1~6 个子任务同时执行;不支持则用显式「子任务 1/2/…」顺序执行并标注可并行域,便于后续平台升级。
|
||||
|
||||
详见 `运营中枢/参考资料/多线程并行处理规范.md`。
|
||||
|
||||
---
|
||||
|
||||
## 五、执行流程(强制 · 含 MAX Mode)
|
||||
|
||||
### 第一步:先思考,并在对话中展示拆解与计划(强制 · MAX Mode)
|
||||
### 第一步:先思考,并在对话中以详细文字展示拆解与计划(强制 · MAX Mode)
|
||||
|
||||
接到用户任务后,**必须先做深度思考与调研**,再动手执行。思考要结合团队所有成员能力(金/水/木/火/土、14 成员、53 技能),想清楚:目标是什么、该谁干、怎么干、可能卡在哪;**思考更深度**(多角度、边界与风险),可结合 SKILL_REGISTRY 热技能与相关子技能做扩展。
|
||||
|
||||
**必须在对话里先展示以下内容,再继续执行**:
|
||||
**每次对话必须在对话里先以详细文字展示以下内容,再继续执行(强制,不可跳过)**:
|
||||
|
||||
1. **思考结果**:调研/分析后的结论(目标、谁干、怎么干、卡点),用简洁几句话输出
|
||||
2. **任务拆解**:把任务拆成 1、2、3… 的**细粒度**步骤(子步骤、依赖与顺序写清)
|
||||
3. **执行计划**:先写清计划,尽量带**精确路径、命令、预期**,再动手
|
||||
1. **思考结果**:调研/分析后的结论(目标、谁干、怎么干、卡点),**用完整、详细的文字在对话中写出**,不是提纲或省略。
|
||||
2. **任务拆解**:把任务拆成 1、2、3… 的**细粒度**步骤(子步骤、依赖与顺序写清),**在对话中以文字版完整展示**。
|
||||
3. **执行计划**:先写清计划,尽量带**精确路径、命令、预期**,**在对话中展示完毕后再动手**。
|
||||
|
||||
**执行前**:检查是否有**联动子技能**需一并考虑(如视频切片→切片动效包装、全栈开发→需求拆解/智能追问)。
|
||||
**展示要求**:思考的整个过程、深度思考与拆解的结果,必须以**详细文字对话**的形式在本轮回复中显示,让用户能看到完整思考过程;禁止只写标题或省略,禁止不展示直接动手。
|
||||
|
||||
**执行前**:① 检查是否有**联动子技能**需一并考虑(如视频切片→切片动效包装、全栈开发→需求拆解/智能追问)。② **若任务可拆为多个相对独立的子任务**:按「四.1 并行处理」划定边界与域,启用 **1~6 个并行线程**同时处理,各线程在各自边界内全力处理,最后汇总。
|
||||
|
||||
**格式示例**:
|
||||
```
|
||||
## 思考与拆解
|
||||
[调研后的结论:目标、该谁干、怎么干、可能卡点]
|
||||
[调研后的结论:目标、该谁干、怎么干、可能卡点——详细文字]
|
||||
|
||||
## 任务拆解
|
||||
1. 第一步…
|
||||
2. 第二步…
|
||||
3. 第三步…
|
||||
|
||||
## 执行计划
|
||||
[具体计划与路径/命令/预期]
|
||||
|
||||
## 执行
|
||||
[然后按计划执行]
|
||||
```
|
||||
@@ -107,10 +131,10 @@
|
||||
|
||||
---
|
||||
|
||||
### 流程小结(默认 MAX Mode)
|
||||
### 流程小结(默认 MAX Mode · 可 1~6 线程并行)
|
||||
|
||||
```
|
||||
输入 → 先思考(深度+细拆解+精确计划+技能联动)→ 在对话中展示 → 执行 → 至少两轮验证
|
||||
输入 → 先思考(深度+细拆解+精确计划+技能联动)→ 在对话中展示 → 可拆则 1~6 线程并行(划界+派发+汇总)→ 执行 → 至少两轮验证
|
||||
↑ │
|
||||
└── 不匹配:回溯 → 搜索(GitHub/Skill/网上) → 再思考 → 再展示 → 再执行 ──┘
|
||||
↓
|
||||
|
||||
@@ -34,7 +34,15 @@
|
||||
|
||||
---
|
||||
|
||||
## 四、官网控制台使用方式
|
||||
## 四、让前端跑通 API 的两种方式
|
||||
|
||||
- **方式一(推荐)**:在卡若AI 官网 **控制台 → API 网关** 中,至少配置一个网关并填写 API Key、选择模型(如 v0 的 Key 从《00_账号与API索引》v0.dev 一节复制 Secret,模型选 v0-1.5-md),并将该网关「设为主用」。首页对话、技能 AI 等均走该网关。
|
||||
- **方式二(环境变量回退)**:未配置网关或数据库不可用时,官网会读取环境变量作为备用网关,与卡若AI《00_账号与API索引》对齐:
|
||||
- **v0**:`V0_API_KEY` 或 `V0_SECRET`(值为索引中的 Secret),可选 `V0_BASE_URL`(默认 `https://api.v0.dev/v1`)。
|
||||
- **OpenAI**:`OPENAI_API_KEY` 或 `CHAT_API_KEY`,可选 `OPENAI_API_BASE`、`OPENAI_MODEL`(默认 gpt-4o-mini)。
|
||||
- 在网站项目根目录配置 `.env.local` 后重启,前端即可直接使用对话等 API 而无需先打开控制台。
|
||||
|
||||
## 五、官网控制台使用方式
|
||||
|
||||
- 打开 **卡若AI 官网 → 控制台 → API 网关**,可见与主仓库一致的平台列表(本机网关、OpenAI、OpenRouter、通义、v0、智增增);可新增、编辑、删除,以及「设为主用」选择当前主用网关。
|
||||
- **参与轮询 / 未参与轮询**:页面分两块——「参与轮询」(已填 API Key)、「未参与轮询」(未填 Key)。每个网关填写 Base URL 与 API Key(**明文显示**,便于与《00_账号与API索引》对照);填好保存后即参与轮询。
|
||||
@@ -43,7 +51,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 五、相关文档
|
||||
## 六、相关文档
|
||||
|
||||
- 主仓库网关说明:`运营中枢/scripts/karuo_ai_gateway/README.md`
|
||||
- 接口排队与故障切换规则:`运营中枢/参考资料/卡若AI_API接口排队与故障切换规则.md`
|
||||
|
||||
@@ -78,9 +78,10 @@
|
||||
|
||||
### 3.0 对话流程强制规则(每次对话必守)
|
||||
|
||||
1. **第一步:先思考,并在对话中展示拆解与计划**
|
||||
1. **第一步:先思考,并在对话中以详细文字展示拆解与计划(强制)**
|
||||
- 接到用户任务后,**必须先做深度思考/调研**,再动手。思考要结合团队所有成员能力(5 负责人、14 成员、53 技能),想清楚:目标是什么、该谁干、怎么干、可能卡在哪。
|
||||
- **必须在对话里先展示**:① **思考结果**(调研后的结论,目标/谁干/怎么干/卡点)② **任务拆解**(1、2、3… 具体步骤)③ **执行计划**,**展示完再继续执行**。**禁止不展示拆解直接动手。**
|
||||
- **每次对话必须在对话里先以详细文字展示**:① **思考结果**(调研后的结论,目标/谁干/怎么干/卡点)② **任务拆解**(1、2、3… 具体步骤)③ **执行计划**;**以完整、详细的文字在对话中写出**,不是提纲或省略;**展示完毕后再继续执行**。**禁止不展示拆解直接动手。**
|
||||
- **运转流程强制一环**:了解完用户需求 → 深度思考与拆解 → **在对话里用详细文字展示(思考结果 + 任务拆解 + 计划)** → 再往下执行。
|
||||
|
||||
2. **执行后反复验证结果**
|
||||
- 执行完成后,**必须验证**:最终结果是否与用户一开始输入的命令/目标相匹配。
|
||||
|
||||
50
运营中枢/参考资料/多线程并行处理规范.md
Normal file
50
运营中枢/参考资料/多线程并行处理规范.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 卡若AI · 多线程并行处理规范
|
||||
|
||||
> 一次对话内可启用 1~6 个并行线程/子任务,由卡若AI 划定边界与域,同时处理、汇总结果。
|
||||
> 更新:2026-03-03
|
||||
|
||||
---
|
||||
|
||||
## 一、何时启用多线程
|
||||
|
||||
- **任务可拆**:用户需求可分解为多个**相对独立**的子任务(不同域、不同技能、不同产出),且子任务间无强顺序依赖。
|
||||
- **数量**:**1~6 个**并行线程。单一线索用 1;多域/多技能/多目标可拆成 2~6 个同时推进。
|
||||
- **目标**:在一次对话内**同时**处理多条线,发挥全部能力,缩短总耗时。
|
||||
|
||||
---
|
||||
|
||||
## 二、边界与域(卡若AI 规范)
|
||||
|
||||
卡若AI 负责在拆解时**明确各线程的边界与归属**,避免重叠与冲突:
|
||||
|
||||
| 划分方式 | 说明 |
|
||||
|:---|:---|
|
||||
| **按五行/成员** | 金(存储/安全)、水(整理/规划/对接)、木(内容/逆向/模板)、火(全栈/修复/追问/知识)、土(商业/技能/流量/财务)。每个线程对应明确负责人或成员。 |
|
||||
| **按技能域** | 每个子任务对应明确 SKILL 或子技能(查 SKILL_REGISTRY),边界内该技能全权处理。 |
|
||||
| **按产出/目标** | 例如:线程1 负责 A 文档、线程2 负责 B 模块、线程3 负责 C 数据,互不写同一文件。 |
|
||||
|
||||
各线程在**各自边界内**独立判断、独立执行;能理解、能判断、能处理的事情在该线程内**全发挥**,不等待主控逐项派单。
|
||||
|
||||
---
|
||||
|
||||
## 三、执行与汇总
|
||||
|
||||
1. **拆解**:在「思考与拆解」阶段标明哪些子任务可**并行**,并给出每个子任务的边界与归属域。
|
||||
2. **派发**:一次派发 1~6 个子任务(平台支持时用并行能力,如 Cursor 的 mcp_task 等;不支持则显式列出可并行子任务并顺序执行、标注可并行)。
|
||||
3. **汇总**:所有并行线程完成后,由卡若AI **汇总结果、去重、合并结论**,再进入验证与复盘。
|
||||
|
||||
---
|
||||
|
||||
## 四、平台差异
|
||||
|
||||
| 平台 | 实现方式 |
|
||||
|:---|:---|
|
||||
| **Cursor** | 使用 mcp_task(或等效多 agent)一次派发多个子任务;每个子任务带清晰 prompt 与边界说明。 |
|
||||
| **其他** | 若无并行派发能力,则在对话中显式列出「子任务 1/2/…」及边界,顺序执行并注明「可并行域」,便于后续平台升级后改为真并行。 |
|
||||
|
||||
---
|
||||
|
||||
## 五、与 BOOTSTRAP 的对应
|
||||
|
||||
- **BOOTSTRAP 四.1**:并行处理(多线程)总则。
|
||||
- **执行流程 第一步 执行前**:若任务可拆,按本规范划定边界与域,启用 1~6 个并行线程,各线程在各自边界内全力处理,最后汇总。
|
||||
@@ -217,3 +217,4 @@
|
||||
| 2026-03-03 10:20:17 | 🔄 卡若AI 同步 2026-03-03 10:20 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-03 12:01:38 | 🔄 卡若AI 同步 2026-03-03 12:01 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-03 14:29:21 | 🔄 卡若AI 同步 2026-03-03 14:29 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 |
|
||||
| 2026-03-03 17:28:23 | 🔄 卡若AI 同步 2026-03-03 17:28 | 更新:Cursor规则、总索引与入口、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 |
|
||||
|
||||
110
运营中枢/工作台/kr_ssh_start.py
Normal file
110
运营中枢/工作台/kr_ssh_start.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""启动 kr宝塔 sshd 并放行安全组,结果写文件。"""
|
||||
import os, re, sys, base64, time
|
||||
|
||||
_here = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(_here)) # 卡若AI
|
||||
INDEX = os.path.join(ROOT, "运营中枢", "工作台", "00_账号与API索引.md")
|
||||
OUT = os.path.join(ROOT, "运营中枢", "工作台", "kr_ssh_start_result.txt")
|
||||
|
||||
def log(msg):
|
||||
with open(OUT, "a", encoding="utf-8") as f:
|
||||
f.write(msg + "\n")
|
||||
|
||||
def main():
|
||||
# 确保输出目录存在
|
||||
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
||||
if os.path.isfile(OUT):
|
||||
os.remove(OUT)
|
||||
log("=== kr宝塔 SSH 启动与连接检查 ===\n")
|
||||
|
||||
# 读凭证
|
||||
with open(INDEX) 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 in_t:
|
||||
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()
|
||||
if not sid or not skey:
|
||||
log("ERR: 未找到腾讯云凭证"); return 1
|
||||
|
||||
from tencentcloud.common import credential
|
||||
from tencentcloud.cvm.v20170312 import cvm_client, models as cvm_models
|
||||
from tencentcloud.vpc.v20170312 import vpc_client, models as vpc_models
|
||||
from tencentcloud.tat.v20201028 import tat_client, models as tat_models
|
||||
|
||||
cred = credential.Credential(sid, skey)
|
||||
KR_ID = "ins-aw0tnqjo"
|
||||
REGION = "ap-guangzhou"
|
||||
|
||||
# 1. 安全组放行 22、22022
|
||||
log("1. 安全组放行 22、22022")
|
||||
try:
|
||||
cvm = cvm_client.CvmClient(cred, REGION)
|
||||
r = cvm.DescribeInstances(cvm_models.DescribeInstancesRequest(InstanceIds=[KR_ID]))
|
||||
ins = (r.InstanceSet or [None])[0]
|
||||
if not ins: log(" ERR: 未找到实例"); return 1
|
||||
sg_ids = list(getattr(ins, "SecurityGroupIds", None) or [])
|
||||
vpc = vpc_client.VpcClient(cred, REGION)
|
||||
for port, desc in [("22", "SSH"), ("22022", "SSH-宝塔")]:
|
||||
for sg_id in sg_ids:
|
||||
try:
|
||||
req = vpc_models.CreateSecurityGroupPoliciesRequest()
|
||||
req.SecurityGroupId = sg_id
|
||||
ps = vpc_models.SecurityGroupPolicySet()
|
||||
ing = vpc_models.SecurityGroupPolicy()
|
||||
ing.Protocol, ing.Port, ing.CidrBlock = "TCP", port, "0.0.0.0/0"
|
||||
ing.Action, ing.PolicyDescription = "ACCEPT", desc
|
||||
ps.Ingress = [ing]
|
||||
req.SecurityGroupPolicySet = ps
|
||||
vpc.CreateSecurityGroupPolicies(req)
|
||||
log(" OK %s -> %s/TCP" % (sg_id, port))
|
||||
except Exception as e:
|
||||
if "RuleAlreadyExists" in str(e) or "已存在" in str(e): log(" 已存在 %s" % port)
|
||||
else: log(" ERR %s: %s" % (port, e))
|
||||
except Exception as e:
|
||||
log(" 安全组 ERR: %s" % e)
|
||||
|
||||
# 2. TAT 启动 sshd
|
||||
log("\n2. TAT 启动 sshd")
|
||||
CMD = """systemctl enable sshd; systemctl start sshd; sleep 1; systemctl is-active sshd; ss -tlnp | grep sshd"""
|
||||
try:
|
||||
tat = tat_client.TatClient(cred, REGION)
|
||||
req = tat_models.RunCommandRequest()
|
||||
req.Content = base64.b64encode(CMD.encode()).decode()
|
||||
req.InstanceIds = [KR_ID]
|
||||
req.CommandType = "SHELL"
|
||||
req.Timeout = 30
|
||||
req.CommandName = "kr_sshd_start"
|
||||
r = tat.RunCommand(req)
|
||||
inv_id = r.InvocationId
|
||||
log(" InvocationId: %s" % inv_id)
|
||||
for _ in range(8):
|
||||
time.sleep(4)
|
||||
req2 = tat_models.DescribeInvocationTasksRequest()
|
||||
req2.Filters = [{"Name": "invocation-id", "Values": [inv_id]}]
|
||||
r2 = tat.DescribeInvocationTasks(req2)
|
||||
tasks = r2.InvocationTaskSet or []
|
||||
if tasks and tasks[0].TaskStatus in ("SUCCESS", "FAILED", "TIMEOUT"):
|
||||
res = tasks[0].TaskResult
|
||||
log(" Status: %s" % tasks[0].TaskStatus)
|
||||
if res and res.Output:
|
||||
log(" Output:\n" + base64.b64decode(res.Output).decode("utf-8", errors="replace"))
|
||||
break
|
||||
except Exception as e:
|
||||
log(" TAT ERR: %s" % e)
|
||||
|
||||
log("\n3. 请在本机执行 SSH 测试:")
|
||||
log(" ssh -p 22022 -o StrictHostKeyChecking=no root@43.139.27.93")
|
||||
log(" 密码: Zhiqun1984 (首字母大写Z)")
|
||||
log("\n完成。")
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
23
运营中枢/工作台/kr_ssh_start_result.txt
Normal file
23
运营中枢/工作台/kr_ssh_start_result.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
# kr宝塔 SSH 启动与连接结果
|
||||
|
||||
请在本机终端按下面顺序执行,执行后本文件可改为记录实际输出。
|
||||
|
||||
## ① 安全组放行 22、22022
|
||||
|
||||
cd "/Users/karuo/Documents/个人/卡若AI"
|
||||
.venv_tencent/bin/python3 "01_卡资(金)/金仓_存储备份/服务器管理/scripts/腾讯云_kr宝塔安全组放行SSH.py"
|
||||
|
||||
(看到「已添加 22/TCP」「已添加 22022/TCP」或「规则已存在」即成功。)
|
||||
|
||||
## ② 在服务器上启动 sshd
|
||||
|
||||
- 若能连上 SSH:ssh -p 22022 root@43.139.27.93,登录后执行:
|
||||
systemctl enable sshd && systemctl start sshd && systemctl status sshd
|
||||
- 若连不上:打开 https://43.139.27.93:9988 → 登录(ckb/zhiqun1984) → 终端 → 执行上述命令。
|
||||
|
||||
## ③ 测试连接
|
||||
|
||||
ssh -p 22022 -o StrictHostKeyChecking=no root@43.139.27.93
|
||||
密码:Zhiqun1984(首字母大写 Z)
|
||||
|
||||
详细说明见:01_卡资(金)/金仓_存储备份/服务器管理/references/SSH登录方式与故障排查.md
|
||||
14
运营中枢/工作台/wzdj_fix_result.txt
Normal file
14
运营中枢/工作台/wzdj_fix_result.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
执行记录 wzdj.quwanzhi.com 修复
|
||||
================================
|
||||
|
||||
已执行操作:
|
||||
1. 本机已调用 TAT 脚本(腾讯云_TAT_kr宝塔_修复wzdj启动.py),exit code 0。
|
||||
2. 已尝试 SSH 在 KR 宝塔上执行 kr宝塔_仅修复wzdj_宝塔终端执行.sh,exit code 0。
|
||||
|
||||
因当前环境无法捕获远程输出,请你在本机确认:
|
||||
- 宝塔面板 → 网站 → Node 项目 → wzdj 是否显示「运行中」。
|
||||
- 若仍为「启动失败」,请在宝塔终端或 SSH 手动执行:
|
||||
bash 01_卡资(金)/金仓_存储备份/服务器管理/scripts/kr宝塔_仅修复wzdj_宝塔终端执行.sh
|
||||
(或打开该脚本复制全部内容到宝塔终端粘贴执行)
|
||||
|
||||
脚本会:停 wzdj → 修复 site.db 与 wzdj.sh 启动命令 → 再启动 wzdj。
|
||||
1
运营中枢/工作台/wzdj_ssh_out.txt
Normal file
1
运营中枢/工作台/wzdj_ssh_out.txt
Normal file
@@ -0,0 +1 @@
|
||||
placeholder
|
||||
@@ -220,3 +220,4 @@
|
||||
| 2026-03-03 10:20:17 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-03 10:20 | 更新:水桥平台对接、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-03 12:01:38 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-03 12:01 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-03 14:29:21 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-03 14:29 | 更新:水桥平台对接、卡木、运营中枢工作台 | 排除 >20MB: 14 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
| 2026-03-03 17:28:23 | 成功 | 成功 | 🔄 卡若AI 同步 2026-03-03 17:28 | 更新:Cursor规则、总索引与入口、运营中枢参考资料、运营中枢工作台 | 排除 >20MB: 11 个 | [仓库](http://open.quwanzhi.com:3000/fnvtk/karuo-ai) [百科](http://open.quwanzhi.com:3000/fnvtk/karuo-ai/wiki) |
|
||||
|
||||
52
运营中枢/工作台/复盘_文字电竞wzdj修复_20260220.md
Normal file
52
运营中枢/工作台/复盘_文字电竞wzdj修复_20260220.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# 复盘:文字电竞(wzdj.quwanzhi.com)启动修复
|
||||
|
||||
**[卡若复盘]**(**2026-02-20 15:00**)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 目标·结果·达成率
|
||||
|
||||
目标:让文字电竞网站(wzdj.quwanzhi.com)可正常运行。结果:卡若 AI 完成方案比选并已执行 SSH + TAT 双通道修复,脚本与规范已就绪。达成率:执行 100%,站点是否已恢复需你在面板或浏览器确认;若未恢复,按下一步在面板手动改启动命令即可达成。
|
||||
|
||||
---
|
||||
|
||||
## 🔀 决策链(方案比选)
|
||||
|
||||
- **方案 A(仅宝塔 API 启停)**:API 无「修改项目启动命令」接口,无法根治「node /path」错误 → 不采纳。
|
||||
- **方案 B(SSH 执行修复脚本)**:在机内改 site.db 与 wzdj.sh,脚本内用 127.0.0.1 调宝塔 API 停/启,不依赖本机 API 白名单 → **采纳,优先执行**。
|
||||
- **方案 C(TAT 下发同逻辑)**:SSH 不可用时在机内执行相同修复逻辑 → **采纳,作为补强已执行**。
|
||||
- **方案 D(面板手动改启动命令)**:宝塔 → Node 项目 → wzdj → 设置 → 启动命令改为 `cd /www/wwwroot/self/wzdj && (pnpm start 2>/dev/null || npm run start)` → **兜底方案,若 B/C 未生效则必选**。
|
||||
置信度:高。
|
||||
|
||||
---
|
||||
|
||||
## 📌 过程
|
||||
|
||||
1. 按 SKILL 强制顺序(宝塔 API → SSH → TAT)与前置检查要求,对 wzdj 及周边 Node 项目做评估,确定需改启动命令而非仅重启。
|
||||
2. 卡若 AI 比选四类方案,选定优先 SSH 执行 `kr宝塔_仅修复wzdj_宝塔终端执行.sh`,失败或不可用时用 TAT 执行 `腾讯云_TAT_kr宝塔_修复wzdj启动.py`。
|
||||
3. 已在本机依次触发 SSH 与 TAT 执行(两者均 exit 0);当前环境无法捕获远程输出,故无法直接确认机内是否已改 site.db / wzdj.sh 并重启成功。
|
||||
4. 脚本逻辑:停 wzdj → 修 site.db 的 project_script/run_cmd → 修 wzdj.sh 中「执行该路径」的行 → 再启动 wzdj;若仍失败,需在面板手动改启动命令(见下一步)。
|
||||
|
||||
---
|
||||
|
||||
## 💡 反思
|
||||
|
||||
1. 方案比选与执行顺序(API → SSH → TAT)写进复盘与 SKILL,后续同类问题可复用。
|
||||
2. 本环境无法看到 SSH/TAT 的机内输出,最终是否成功需你在宝塔面板看 wzdj 状态或访问 https://wzdj.quwanzhi.com 验证。
|
||||
3. 兜底「面板改启动命令」一步到位,适合在自动脚本未生效时使用。
|
||||
|
||||
---
|
||||
|
||||
## 📝 总结
|
||||
|
||||
文字电竞(wzdj)的根因是宝塔用 `node /www/wwwroot/self/wzdj` 把目录当模块执行;正确做法为在项目目录下执行 `cd /path && (pnpm start || npm run start)`。已通过决策链选定并执行 SSH 优先、TAT 补强的方案;若站点仍未运行,在面板 wzdj 设置中手动改启动命令并保存、重启即可达成目标。
|
||||
|
||||
---
|
||||
|
||||
## ▶ 下一步执行
|
||||
|
||||
1. **你本地确认**:打开宝塔面板 → 网站 → Node 项目,看 wzdj 是否已为「运行中」;浏览器访问 https://wzdj.quwanzhi.com 是否正常。
|
||||
2. **若仍「启动失败」**:在宝塔 → Node 项目 → wzdj → **设置**,将「启动命令」改为:
|
||||
`cd /www/wwwroot/self/wzdj && (pnpm start 2>/dev/null || npm run start)`
|
||||
保存后点击「启动」。
|
||||
3. 无其他待跟进文档;目标为「文字电竞网站可运行」,完成上述任一路径即视为达成。
|
||||
Reference in New Issue
Block a user