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)"
|
||||
fi
|
||||
|
||||
# 递归加密整个 scripts/(含 cli、service、db、util 等子包);仅 scripts/*.py 会漏模块导致运行时 ModuleNotFoundError。
|
||||
# 递归加密整个 scripts/(含 cli、service、db、util 等子包);产物保留与源码一致的 scripts/ 层级,入口为 scripts/main.py。
|
||||
- name: Encrypt Source Code
|
||||
run: |
|
||||
mkdir -p dist/package
|
||||
mkdir -p dist/package/scripts
|
||||
set -euo pipefail
|
||||
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/
|
||||
if [ -d references ]; then cp -r references dist/package/; fi
|
||||
if [ -d assets ]; then cp -r assets dist/package/; fi
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
from .client import EntitlementClient
|
||||
from .guard import enforce_entitlement
|
||||
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__ = [
|
||||
"EntitlementClient",
|
||||
"EntitlementResult",
|
||||
"apply_cli_local_defaults",
|
||||
"attach_unified_file_handler",
|
||||
"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+
|
||||
|
||||
加密与 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/ 结构自检,避免子目录未提交却仍发布。
|
||||
#>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user