feat: release ZIP keeps scripts/ tree; JIANGCHANG_SKILLS_ROOT and sibling skills root
This commit is contained in:
@@ -58,13 +58,13 @@ jobs:
|
|||||||
python -c "import os,base64,pathlib,subprocess; p=pathlib.Path('/tmp/pyarmor-reg.zip'); p.write_bytes(base64.standard_b64decode(os.environ['PYARMOR_REG_B64'])); subprocess.run(['pyarmor','reg',str(p)],check=True); p.unlink(missing_ok=True)"
|
python -c "import os,base64,pathlib,subprocess; p=pathlib.Path('/tmp/pyarmor-reg.zip'); p.write_bytes(base64.standard_b64decode(os.environ['PYARMOR_REG_B64'])); subprocess.run(['pyarmor','reg',str(p)],check=True); p.unlink(missing_ok=True)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 递归加密整个 scripts/(含 cli、service、db、util 等子包);仅 scripts/*.py 会漏模块导致运行时 ModuleNotFoundError。
|
# 递归加密整个 scripts/(含 cli、service、db、util 等子包);产物保留与源码一致的 scripts/ 层级,入口为 scripts/main.py。
|
||||||
- name: Encrypt Source Code
|
- name: Encrypt Source Code
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist/package
|
mkdir -p dist/package/scripts
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
test -d scripts
|
test -d scripts
|
||||||
( cd scripts && pyarmor gen --platform "${PYARMOR_PLATFORM}" -r -O ../dist/package . )
|
( cd scripts && pyarmor gen --platform "${PYARMOR_PLATFORM}" -r -O ../dist/package/scripts . )
|
||||||
cp SKILL.md dist/package/
|
cp SKILL.md dist/package/
|
||||||
if [ -d references ]; then cp -r references dist/package/; fi
|
if [ -d references ]; then cp -r references dist/package/; fi
|
||||||
if [ -d assets ]; then cp -r assets dist/package/; fi
|
if [ -d assets ]; then cp -r assets dist/package/; fi
|
||||||
|
|||||||
@@ -1,9 +1,39 @@
|
|||||||
from .client import EntitlementClient
|
from .client import EntitlementClient
|
||||||
from .guard import enforce_entitlement
|
from .guard import enforce_entitlement
|
||||||
from .models import EntitlementResult
|
from .models import EntitlementResult
|
||||||
|
from .runtime_env import (
|
||||||
|
apply_cli_local_defaults,
|
||||||
|
get_data_root,
|
||||||
|
get_sibling_skills_root,
|
||||||
|
get_skills_root,
|
||||||
|
get_user_id,
|
||||||
|
platform_default_data_root,
|
||||||
|
)
|
||||||
|
from .unified_logging import (
|
||||||
|
attach_unified_file_handler,
|
||||||
|
ensure_trace_for_process,
|
||||||
|
get_skill_log_file_path,
|
||||||
|
get_skill_logger,
|
||||||
|
get_unified_logs_dir,
|
||||||
|
setup_skill_logging,
|
||||||
|
subprocess_env_with_trace,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"EntitlementClient",
|
"EntitlementClient",
|
||||||
"EntitlementResult",
|
"EntitlementResult",
|
||||||
|
"apply_cli_local_defaults",
|
||||||
|
"attach_unified_file_handler",
|
||||||
"enforce_entitlement",
|
"enforce_entitlement",
|
||||||
|
"ensure_trace_for_process",
|
||||||
|
"get_data_root",
|
||||||
|
"get_sibling_skills_root",
|
||||||
|
"get_skills_root",
|
||||||
|
"get_skill_log_file_path",
|
||||||
|
"get_skill_logger",
|
||||||
|
"get_unified_logs_dir",
|
||||||
|
"get_user_id",
|
||||||
|
"platform_default_data_root",
|
||||||
|
"setup_skill_logging",
|
||||||
|
"subprocess_env_with_trace",
|
||||||
]
|
]
|
||||||
|
|||||||
127
sdk/jiangchang_skill_core/runtime_env.py
Normal file
127
sdk/jiangchang_skill_core/runtime_env.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
"""
|
||||||
|
JIANGCHANG_* 数据根与用户目录:解析规则 + 可选本地 CLI 默认值。
|
||||||
|
|
||||||
|
各技能 scripts/jiangchang_skill_core/ 为发布用副本,与 kit 保持同步。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 发版/嵌入宿主前改为 False,或仅通过环境变量 JIANGCHANG_CLI_LOCAL_DEV=1 开启
|
||||||
|
CLI_LOCAL_DEV_ENABLED = True
|
||||||
|
|
||||||
|
# 本地开发且未设置 JIANGCHANG_USER_ID 时注入(与宿主约定一致即可修改)
|
||||||
|
DEFAULT_LOCAL_USER_ID = "10032"
|
||||||
|
|
||||||
|
# Windows 下未设置 JIANGCHANG_DATA_ROOT 时的默认盘路径
|
||||||
|
WIN_DEFAULT_DATA_ROOT = r"D:\jiangchang-data"
|
||||||
|
|
||||||
|
# 匠厂桌面宿主应用根(未设置 JIANGCHANG_APP_ROOT 时 Windows 兜底;技能一般在 {APP}/skills/ 下并列)
|
||||||
|
WIN_DEFAULT_JIANGCHANG_APP_ROOT = r"D:\AI\jiangchang"
|
||||||
|
|
||||||
|
|
||||||
|
def platform_default_data_root() -> str:
|
||||||
|
if sys.platform == "win32":
|
||||||
|
return WIN_DEFAULT_DATA_ROOT
|
||||||
|
return os.path.join(os.path.expanduser("~"), ".jiangchang-data")
|
||||||
|
|
||||||
|
|
||||||
|
def get_data_root() -> str:
|
||||||
|
env = (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip()
|
||||||
|
if env:
|
||||||
|
return env
|
||||||
|
return platform_default_data_root()
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_id() -> str:
|
||||||
|
return (os.getenv("JIANGCHANG_USER_ID") or "").strip() or "_anon"
|
||||||
|
|
||||||
|
|
||||||
|
def _looks_like_skills_root(path: str) -> bool:
|
||||||
|
if not path or not os.path.isdir(path):
|
||||||
|
return False
|
||||||
|
for marker in (
|
||||||
|
"llm-manager",
|
||||||
|
"content-manager",
|
||||||
|
"account-manager",
|
||||||
|
"sohu-publisher",
|
||||||
|
"api-key-vault",
|
||||||
|
):
|
||||||
|
if os.path.isdir(os.path.join(path, marker)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_skills_root() -> str:
|
||||||
|
"""
|
||||||
|
并列技能安装目录(其下为「技能 slug」子目录)。
|
||||||
|
|
||||||
|
优先级:
|
||||||
|
1) JIANGCHANG_SKILLS_ROOT
|
||||||
|
2) CLAW_SKILLS_ROOT
|
||||||
|
3) JIANGCHANG_APP_ROOT:若 {APP}/skills 存在且像技能根则用之;否则若 APP 下已有技能子目录则 APP 即根
|
||||||
|
4) Windows:默认 D:\\AI\\jiangchang 同上规则
|
||||||
|
5) 其他平台:~/.openclaw/skills
|
||||||
|
"""
|
||||||
|
for key in ("JIANGCHANG_SKILLS_ROOT", "CLAW_SKILLS_ROOT"):
|
||||||
|
v = (os.getenv(key) or "").strip()
|
||||||
|
if v:
|
||||||
|
return os.path.normpath(v)
|
||||||
|
|
||||||
|
app = (os.getenv("JIANGCHANG_APP_ROOT") or "").strip()
|
||||||
|
if sys.platform == "win32" and not app:
|
||||||
|
app = WIN_DEFAULT_JIANGCHANG_APP_ROOT
|
||||||
|
if app:
|
||||||
|
nested = os.path.join(app, "skills")
|
||||||
|
if _looks_like_skills_root(nested):
|
||||||
|
return os.path.normpath(nested)
|
||||||
|
if _looks_like_skills_root(app):
|
||||||
|
return os.path.normpath(app)
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
nested = os.path.join(WIN_DEFAULT_JIANGCHANG_APP_ROOT, "skills")
|
||||||
|
if _looks_like_skills_root(nested):
|
||||||
|
return os.path.normpath(nested)
|
||||||
|
if _looks_like_skills_root(WIN_DEFAULT_JIANGCHANG_APP_ROOT):
|
||||||
|
return os.path.normpath(WIN_DEFAULT_JIANGCHANG_APP_ROOT)
|
||||||
|
|
||||||
|
return os.path.normpath(os.path.join(os.path.expanduser("~"), ".openclaw", "skills"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_sibling_skills_root(skill_scripts_dir: str | None = None) -> str:
|
||||||
|
"""
|
||||||
|
编排子进程调用兄弟技能时用:优先环境变量;否则从本技能 scripts 目录推断并列根
|
||||||
|
(OpenClaw 开发仓与网关 skills/<slug>/scripts 均满足「技能根之上一级 = 并列根」)。
|
||||||
|
"""
|
||||||
|
for key in ("JIANGCHANG_SKILLS_ROOT", "CLAW_SKILLS_ROOT"):
|
||||||
|
v = (os.getenv(key) or "").strip()
|
||||||
|
if v:
|
||||||
|
return os.path.normpath(v)
|
||||||
|
|
||||||
|
if skill_scripts_dir:
|
||||||
|
scripts = os.path.abspath(skill_scripts_dir)
|
||||||
|
skill_root = os.path.dirname(scripts)
|
||||||
|
inferred = os.path.dirname(skill_root)
|
||||||
|
if _looks_like_skills_root(inferred):
|
||||||
|
return inferred
|
||||||
|
|
||||||
|
return get_skills_root()
|
||||||
|
|
||||||
|
|
||||||
|
def apply_cli_local_defaults() -> None:
|
||||||
|
"""
|
||||||
|
在 CLI 最早阶段调用(main.py 在 import 业务包之前)。
|
||||||
|
宿主已设置 JIANGCHANG_* 时不会覆盖。
|
||||||
|
"""
|
||||||
|
enabled = CLI_LOCAL_DEV_ENABLED
|
||||||
|
if not enabled:
|
||||||
|
v = (os.getenv("JIANGCHANG_CLI_LOCAL_DEV") or "").strip().lower()
|
||||||
|
enabled = v in ("1", "true", "yes", "on")
|
||||||
|
if not enabled:
|
||||||
|
return
|
||||||
|
if not (os.getenv("JIANGCHANG_DATA_ROOT") or "").strip():
|
||||||
|
os.environ["JIANGCHANG_DATA_ROOT"] = platform_default_data_root()
|
||||||
|
if not (os.getenv("JIANGCHANG_USER_ID") or "").strip():
|
||||||
|
os.environ["JIANGCHANG_USER_ID"] = DEFAULT_LOCAL_USER_ID.strip()
|
||||||
158
sdk/jiangchang_skill_core/unified_logging.py
Normal file
158
sdk/jiangchang_skill_core/unified_logging.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
"""
|
||||||
|
统一文件日志:{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log
|
||||||
|
按日轮转;行内带 trace_id 与 skill_slug,便于跨技能排查。
|
||||||
|
|
||||||
|
实现为各技能 scripts/jiangchang_skill_core/ 下的同名副本之源,修改后请同步到各技能。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .runtime_env import get_data_root, get_user_id
|
||||||
|
|
||||||
|
_skill_slug: str = ""
|
||||||
|
_logger_name: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_unified_logs_dir() -> str:
|
||||||
|
path = os.path.join(get_data_root(), get_user_id(), "logs")
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def get_skill_log_file_path() -> str:
|
||||||
|
"""与 setup 写入的主日志文件一致(供终端提示等)。"""
|
||||||
|
override = (os.getenv("JIANGCHANG_LOG_FILE") or "").strip()
|
||||||
|
if override:
|
||||||
|
parent = os.path.dirname(os.path.abspath(override))
|
||||||
|
if parent:
|
||||||
|
os.makedirs(parent, exist_ok=True)
|
||||||
|
return os.path.abspath(override)
|
||||||
|
return os.path.join(get_unified_logs_dir(), "jiangchang.log")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_trace_for_process() -> str:
|
||||||
|
"""本进程调用链 ID:沿用 JIANGCHANG_TRACE_ID,否则生成并写入环境(子进程可继承)。"""
|
||||||
|
existing = (os.getenv("JIANGCHANG_TRACE_ID") or "").strip()
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
tid = uuid.uuid4().hex[:12]
|
||||||
|
os.environ["JIANGCHANG_TRACE_ID"] = tid
|
||||||
|
return tid
|
||||||
|
|
||||||
|
|
||||||
|
def subprocess_env_with_trace(environ: Optional[dict] = None) -> dict:
|
||||||
|
"""subprocess.run(..., env=...) 时使用,保证带上当前 trace。"""
|
||||||
|
ensure_trace_for_process()
|
||||||
|
tid = (os.getenv("JIANGCHANG_TRACE_ID") or "").strip()
|
||||||
|
if not tid:
|
||||||
|
tid = ensure_trace_for_process()
|
||||||
|
base = os.environ if environ is None else environ
|
||||||
|
return {**base, "JIANGCHANG_TRACE_ID": tid}
|
||||||
|
|
||||||
|
|
||||||
|
class _SkillContextFilter(logging.Filter):
|
||||||
|
def filter(self, record: logging.LogRecord) -> bool:
|
||||||
|
record.trace_id = (os.getenv("JIANGCHANG_TRACE_ID") or "").strip() or "-"
|
||||||
|
record.skill_slug = _skill_slug or "-"
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
_FORMAT = (
|
||||||
|
"%(asctime)s | %(levelname)-8s | %(trace_id)s | %(skill_slug)s | %(name)s | %(message)s"
|
||||||
|
)
|
||||||
|
_DATEFMT = "%Y-%m-%dT%H:%M:%S"
|
||||||
|
|
||||||
|
|
||||||
|
def _log_level_from_env() -> int:
|
||||||
|
v = (os.getenv("JIANGCHANG_LOG_LEVEL") or "INFO").strip().upper()
|
||||||
|
return getattr(logging, v, None) or logging.INFO
|
||||||
|
|
||||||
|
|
||||||
|
def _backup_count() -> int:
|
||||||
|
try:
|
||||||
|
n = int((os.getenv("JIANGCHANG_LOG_BACKUP_COUNT") or "30").strip())
|
||||||
|
return max(1, min(n, 365))
|
||||||
|
except ValueError:
|
||||||
|
return 30
|
||||||
|
|
||||||
|
|
||||||
|
def setup_skill_logging(skill_slug: str, logger_name: str) -> None:
|
||||||
|
"""
|
||||||
|
幂等:为指定 logger 挂载统一文件日志与可选 stderr。
|
||||||
|
须在进程早期调用(如 CLI main 在业务日志之前)。
|
||||||
|
"""
|
||||||
|
global _skill_slug, _logger_name
|
||||||
|
ensure_trace_for_process()
|
||||||
|
_skill_slug = skill_slug
|
||||||
|
_logger_name = logger_name
|
||||||
|
|
||||||
|
log = logging.getLogger(logger_name)
|
||||||
|
if log.handlers:
|
||||||
|
return
|
||||||
|
log.setLevel(_log_level_from_env())
|
||||||
|
path = get_skill_log_file_path()
|
||||||
|
fh = TimedRotatingFileHandler(
|
||||||
|
path,
|
||||||
|
when="midnight",
|
||||||
|
interval=1,
|
||||||
|
backupCount=_backup_count(),
|
||||||
|
encoding="utf-8",
|
||||||
|
delay=True,
|
||||||
|
)
|
||||||
|
fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
|
||||||
|
fh.setFormatter(fmt)
|
||||||
|
fh.addFilter(_SkillContextFilter())
|
||||||
|
log.addHandler(fh)
|
||||||
|
if (os.getenv("JIANGCHANG_LOG_TO_STDERR") or "").strip().lower() in (
|
||||||
|
"1",
|
||||||
|
"true",
|
||||||
|
"yes",
|
||||||
|
"on",
|
||||||
|
):
|
||||||
|
sh = logging.StreamHandler(sys.stderr)
|
||||||
|
sh.setLevel(logging.WARNING)
|
||||||
|
sh.setFormatter(fmt)
|
||||||
|
sh.addFilter(_SkillContextFilter())
|
||||||
|
log.addHandler(sh)
|
||||||
|
log.propagate = False
|
||||||
|
|
||||||
|
|
||||||
|
def get_skill_logger() -> logging.Logger:
|
||||||
|
if not _logger_name:
|
||||||
|
raise RuntimeError("get_skill_logger: call setup_skill_logging first")
|
||||||
|
return logging.getLogger(_logger_name)
|
||||||
|
|
||||||
|
|
||||||
|
def attach_unified_file_handler(
|
||||||
|
log_path: str,
|
||||||
|
*,
|
||||||
|
skill_slug: str,
|
||||||
|
logger_name: str,
|
||||||
|
level: int = logging.DEBUG,
|
||||||
|
) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
独立子进程内追加同一日志文件(如 login 检测子进程),格式与主进程一致。
|
||||||
|
"""
|
||||||
|
global _skill_slug
|
||||||
|
ensure_trace_for_process()
|
||||||
|
_skill_slug = skill_slug
|
||||||
|
lg = logging.getLogger(logger_name)
|
||||||
|
lg.handlers.clear()
|
||||||
|
lg.setLevel(level)
|
||||||
|
parent = os.path.dirname(os.path.abspath(log_path))
|
||||||
|
if parent:
|
||||||
|
os.makedirs(parent, exist_ok=True)
|
||||||
|
fh = logging.FileHandler(log_path, encoding="utf-8")
|
||||||
|
fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
|
||||||
|
fh.setFormatter(fmt)
|
||||||
|
fh.addFilter(_SkillContextFilter())
|
||||||
|
lg.addHandler(fh)
|
||||||
|
lg.propagate = False
|
||||||
|
return lg
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
Requires: git, PowerShell 5+
|
Requires: git, PowerShell 5+
|
||||||
|
|
||||||
加密与 ZIP 内容由 CI 工作流 reusable-release-skill.yaml 的「Encrypt Source Code」步骤执行:
|
加密与 ZIP 内容由 CI 工作流 reusable-release-skill.yaml 的「Encrypt Source Code」步骤执行:
|
||||||
对 scripts/ 递归 PyArmor(-r),并复制 SKILL.md、references/、assets/(若存在)。
|
对 scripts/ 递归 PyArmor(-r),输出到包内 scripts/(与源码目录树一致),并复制 SKILL.md、references/、assets/(若存在)。
|
||||||
本脚本在打 tag 前会做一次 scripts/ 结构自检,避免子目录未提交却仍发布。
|
本脚本在打 tag 前会做一次 scripts/ 结构自检,避免子目录未提交却仍发布。
|
||||||
#>
|
#>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user