Files
jiangchang-platform-kit/README.md
chendelian cf5e74d80a
All checks were successful
Publish Python Package to Gitea / publish (push) Successful in 20s
feat(activity): SRCP v1 step gates, pause/resume/stop control (v1.2.0)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 12:20:37 +08:00

384 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# jiangchang-platform-kit
## 1. 项目简介
匠厂平台共享 Python SDK在同一发行包中提供 **Skill 侧通用能力**(权益校验、统一日志与运行环境解析)与 **桌面应用自动化能力**(通过 CDP 连接匠厂客户端,驱动聊天与断言)。
## 2. 安装方式
```bash
pip install jiangchang-platform-kit --index-url https://git.jc2009.com/api/packages/client-jiangchang/pypi/simple/
```
## 3. 包含的模块
- **jiangchang_skill_core**:权益 HTTP 客户端、`enforce_entitlement`、统一文件日志、`JIANGCHANG_*` 数据根与技能根路径解析、**Skill Activity Stream / Job 运行时**`emit` / `finish` / Run Journal、兄弟技能 CLI 桥(`sibling_bridge`)、共享媒体资源(`media_assets`)等,供 Skill CLI / 服务进程使用。
- **jiangchang_desktop_sdk**:匠厂桌面应用自动化客户端(基于 CDP + Playwright 同步 API。**本包不声明 Playwright 依赖**;请在运行环境中自行安装 Playwright以便 `import playwright` 可用。
## 4. 快速开始
### jiangchang_skill_core
```python
import os
from jiangchang_skill_core import (
EntitlementClient,
enforce_entitlement,
setup_skill_logging,
get_skill_logger,
)
os.environ.setdefault("JIANGCHANG_AUTH_BASE_URL", "https://auth.example.com")
os.environ.setdefault("JIANGCHANG_AUTH_API_KEY", "secret")
setup_skill_logging(skill_slug="demo-skill", logger_name="demo")
log = get_skill_logger()
log.info("skill started")
# client = EntitlementClient()
# enforce_entitlement(user_id="u1", skill_slug="demo-skill")
```
### jiangchang_desktop_sdk
```python
from jiangchang_desktop_sdk import JiangchangDesktopClient, AskOptions
# 需已安装 playwright且匠厂客户端可按 CDP 端口暴露调试接口
client = JiangchangDesktopClient()
client.ensure_app_running(wait_timeout_s=30.0)
try:
answer = client.ask("你好", options=AskOptions(new_task=True))
print(answer)
finally:
client.disconnect()
```
### 桌面 E2E`jiangchang_desktop_sdk.e2e_helpers`
Skill 的桌面端到端测试应通过宿主 IPC 获取**真实**当前用户数据目录,不要在 pytest 进程里手工设置 `JIANGCHANG_USER_ID` 来伪造用户(否则会回退到 `_anon`)。
推荐 preflight 流程:
1. `require_logged_in_skill_data_dir(page, skill_slug)` — 经 `jiangchang:resolve-skill-data-dir` 解析路径,并断言已登录(非 `_anon` / `anon` / `anonymous`)。
2. `assert_skill_env_file(ctx, ["API_KEY", ...])` — 检查 `{skill_data}/config/.env` 中必填项已配置且非占位符E2E 不会自动生成真实密钥。
3. `send_prompt_via_composer(page, prompt)` — 普通文本逐字键入;内容中的 `\n` / `\r\n` / `\r`**Shift+Enter** 输入换行;最后用 **Enter** 发送(禁止 DOM 注入、剪贴板、JS 伪造 input/change
诊断时可调用 `get_runtime_env(page)` 查看主进程解析的环境,**不要**用它拼用户数据路径。
```python
from jiangchang_desktop_sdk.e2e_helpers import (
assert_skill_env_file,
require_logged_in_skill_data_dir,
send_prompt_via_composer,
)
ctx = require_logged_in_skill_data_dir(page, "demo-skill")
assert_skill_env_file(ctx, ["API_KEY"])
send_prompt_via_composer(page, "第一行\n第二行")
```
## Skill Activity Stream / Job 运行时
长任务批量生成、RPA需要把**逐步进度**推到匠厂宿主 UI而不能依赖 Agent 轮询或 OpenClaw stdout patch。`jiangchang_skill_core.activity` 提供 **Skill Activity Stream (SAS) v1** 契约:
| API | 作用 |
|-----|------|
| `emit(text, ...)` | 写 Run Journal`activity` / `progress` / `warn` / `debug` |
| `step(text)` | `emit(type='activity')` 别名 |
| `finish(status=..., **fields)` | 写终态事件 + **唯一**向 stdout 输出单行 result JSON |
| `rpa_step(name)` | 装饰器:闸门 + 进入/成功/失败自动 emit |
| `checkpoint()` | consult control不写步骤事件 |
| `interruptible_sleep(sec)` | 可暂停/停止的 async sleep |
| `JobStopped` | 宿主 stop 时抛出 |
| `job_context(...)` | 可选包裹 main异常时自动 `finish(failed)` |
### Run Journal 路径(宿主 tail 此文件)
```
{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl
```
- `job_id`:优先 `JIANGCHANG_JOB_ID`,否则进程内 `uuid4().hex`(首次 emit 时生成并缓存)
- `JIANGCHANG_RUN_MODE``job` | `cli`(默认 `cli`),首行 meta 事件记录
- NDJSONUTF-8append-only每行 flush
- `debug` 类型仅当 `JIANGCHANG_ACTIVITY_DEBUG=1` 时写入
### 与 Agent / OpenClaw 的分工
- **`emit()` / journal**:永不 print 到 stdout避免 bash tool 结果淹没 Agent context
- **`finish()`**:唯一允许向 stdout 写业务结果的 API单行 compact JSON`status` + 摘要字段,默认 ≤4096 字符
- 技能里手写 `print("OK")`、pretty JSON 应逐步改为 `finish()`RPA 步骤用 `emit("打开浏览器")``@rpa_step`
### GEO batch 示例
```python
from jiangchang_skill_core.activity import emit, finish
for i, item in enumerate(items, 1):
emit(f"生成 {item['title']}", type="progress", current=i, total=len(items), skill="geo-batch")
# ... business logic ...
finish(status="success", skill="geo-batch", generated=len(items))
```
### RPA 步骤示例
```python
from jiangchang_skill_core.activity import emit, rpa_step
@rpa_step("打开浏览器")
def open_browser(page):
page.goto("https://example.com")
emit("手动步骤说明", skill="publish-toutiao", stage="rpa")
```
`RpaVideoSession.add_step()` 在录屏字幕之外默认同步 `emit(..., stage="rpa.video")`;可通过 `emit_activity=False` 关闭。
## Skill Runtime Control Protocol (SRCP) v1 — 1.2.0+
宿主通过 **control 文件**向技能进程下发暂停 / 继续 / 停止platform-kit 在**步骤闸门**自动读取,技能作者无需手写控制逻辑。
### control.json宿主写、kit 读)
```
{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.control.json
```
```json
{
"schema_version": 1,
"command": "none",
"reason": "user",
"ts": 1730000000000,
"nonce": 1
}
```
| command | 行为 |
|---------|------|
| `none` | 正常运行 |
| `pause` | 下一步闸门进入暂停(阻塞直到 resume 或 stop |
| `resume` | 从暂停恢复 |
| `stop` | 协作式停止,抛出 `JobStopped` |
宿主更新命令时应递增 `nonce`kit 每次闸门重新读文件,不依赖缓存)。
### 步骤闸门(自动挂载)
| 调用点 | 闸门行为 |
|--------|----------|
| `emit()` / `step()` | 进入步骤前 gate + 写 Journal |
| `@rpa_step` | 进入步骤前 gate再写 ▶ / ✓ |
| `RpaVideoSession.add_step()` | 经 `emit()` 自动 gate |
| `interruptible_sleep()` | 每 200ms 切片 `checkpoint()`(可暂停/停止) |
暂停在**当前步骤边界**生效(与影刀「当前指令执行完后暂停」一致),不在 DOM 原子操作中途冻结。
### lifecycle Journal 事件
闸门写入 `type: lifecycle` 事件,供宿主任务中心显示运行 / 暂停 / 继续状态:
```json
{"type":"lifecycle","status":"paused","text":"已暂停,等待继续","step_index":3,"step_name":"打开页面",...}
```
### API
| API | 作用 |
|-----|------|
| `checkpoint()` | 仅 consult control长等待循环内使用 |
| `interruptible_sleep(sec)` | async 可中断 sleep |
| `interruptible_sleep_sync(sec)` | sync 可中断 sleep |
| `JobStopped` | 宿主 stop 时抛出;`job_context` 自动 `finish(..., error_code="JOB_STOPPED")` |
| `get_control_path()` | 当前 job 的 control 文件路径(诊断) |
环境变量:
- `JIANGCHANG_CONTROL_POLL_SEC` — 暂停轮询间隔(默认 `0.2`
- `JIANGCHANG_CONTROL_DISABLED=1` — 禁用闸门(仅测试)
### RPA 示例(拆步 + 可中断等待)
```python
from jiangchang_skill_core.activity import (
emit,
finish,
job_context,
interruptible_sleep,
rpa_step,
)
def cmd_run(...):
with job_context(skill="demo-skill"):
emit("开始任务", skill="demo-skill", stage="run")
@rpa_step("打开后台")
async def open_admin(page):
await page.goto("https://example.com")
await interruptible_sleep(1.5)
await open_admin(page)
finish(status="success", message="完成", skill="demo-skill")
```
技能作者规范以 **skill-template** 为准;本 README 为实现细节参考。
### 兄弟技能 CLI 桥
```python
from jiangchang_skill_core import call_sibling_json, get_sibling_main_path
accounts = call_sibling_json("account-manager", ["list", "all"])
```
同步 `subprocess.run``capture_output=True`,自动带上 `subprocess_env_with_trace()`
## 共享媒体资源
`jiangchang_skill_core.media_assets` 提供共享媒体资源解析能力。默认会从:
`{JIANGCHANG_DATA_ROOT}/shared/media-assets`
读取背景音乐、字体、水印和 ffmpeg 工具。
如果目录不存在,会优先下载稳定 release bundle不依赖用户电脑安装 Git
`https://git.jc2009.com/client-commons/media-assets/releases/download/vlatest/media-assets.zip`
bundle 内含背景音乐、字体与水印;**不包含** ffmpeg 二进制。ffmpeg / ffprobe 仍由 bundle 内 `manifest.json``tools.ffmpeg` 下载源单独拉取。
可通过环境变量覆盖:
- `MEDIA_ASSETS_ROOT=/path/to/custom/media-assets` — 本地资源目录
- `MEDIA_ASSETS_BUNDLE_URL=https://example.com/media-assets.zip` — bundle 下载地址
### Runtime diagnostics统一 health 诊断)
`jiangchang_skill_core.runtime_diagnostics` 提供各 Skill 共用的 Runtime / media-assets / ffmpeg 探测避免每个技能各自复制检查逻辑。Skill 的 `health` 命令可调用:
```python
from jiangchang_skill_core import (
collect_runtime_diagnostics,
format_runtime_health_lines,
runtime_diagnostics_dict,
)
diag = collect_runtime_diagnostics(
skill_slug="my-skill",
platform_kit_min_version="1.0.10", # 可选;省略则只报告当前版本
skill_root="/path/to/my-skill", # 可选;用于检测 vendored jiangchang_skill_core
)
for line in format_runtime_health_lines(diag):
print(line)
print(runtime_diagnostics_dict(diag)) # 可 JSON 序列化
```
诊断只读探测本地状态,不会触发 media-assets 下载。
### 录屏合成(`screencast`
`run_screencast()` / `compose_video()` 通过 `jiangchang_skill_core.media_assets` 解析 ffmpeg 与背景音乐,**不使用系统 PATH 中的 `ffmpeg`**。
- **默认**:不传 `media_assets_root` 时,使用 `{JIANGCHANG_DATA_ROOT}/shared/media-assets`(或进程环境中已有的 `MEDIA_ASSETS_ROOT`)。
- **覆盖**:传入 `media_assets_root="/path/to/media-assets"`,或在 `compose_video(..., media_assets_env={"MEDIA_ASSETS_ROOT": "..."})` 中设置;参数 `media_assets_root` 优先于 `media_assets_env` 里已有的 `MEDIA_ASSETS_ROOT`
- `run_screencast(media_assets_root=...)` 会把该路径原样传给 `compose_video()``music_subdir` 已废弃(会触发 `DeprecationWarning`),音乐选择统一由 `pick_background_music()` 完成。
```python
from screencast import run_screencast
run_screencast(
skill_slug="demo-skill",
subtitle_script=[("PASSED", "测试通过")],
pytest_args=["-q", "tests/test_demo.py"],
output_dir="./out",
media_assets_root="/path/to/media-assets", # 可选
)
```
## 5. 发版流程(维护者)
### 发布规则
- jiangchang-platform-kit **只发布正式版本**
- 每次需要发布公共能力时,先 bump `pyproject.toml``version`(例如 `1.0.10``1.0.11`)。
- 推送 `main` 后,`.github/workflows/publish.yml` 会构建并上传该正式版本到 Gitea PyPI。
- 发布包版本必须等于 `pyproject.toml` 中的 `version`;若该版本已存在,上传会失败,需继续 bump 版本号后再推送。
```bash
git push origin main
```
升级安装:
```bash
python -m pip install --upgrade \
--index-url https://git.jc2009.com/api/packages/client-jiangchang/pypi/simple/ \
--extra-index-url https://pypi.org/simple \
jiangchang-platform-kit
```
### 安装后验证
```bash
python -c "from jiangchang_desktop_sdk.e2e_helpers import send_prompt_via_composer; print('e2e_helpers OK')"
python -c "from screencast import run_screencast; print('screencast OK')"
```
仓库内还保留 `examples/``tools/``python-runtime/` 目录及既有 Skill / 前端相关的 GitHub Actions 工作流,与本 PyPI 包的发布彼此独立。`tools/screencast/` 为兼容薄壳pip 包内实现位于 `screencast` 包(`src/screencast/`)。
## v0.2.0 — data-jcid migration (internal refactor)
### TL;DR
本次内部重构:SDK 不再依赖匠厂主仓库的 `data-jcid` 属性。
所有公开 API(`ask` / `read` / `new_task` / `wait_gateway_ready` 等)
**签名与行为保持不变**,只有内部选择器实现切换到新的语义锚点 + ClawX testid。
### 改变了什么
- `client.py` 内所有 `[data-jcid="..."]` 选择器已移除
- 改用匠厂主仓库为 SDK 承诺的语义锚点:`data-role` / `data-message-id` /
`data-streaming`(在 ChatMessage 上)、`data-sending` / `data-message-count`
(在 chat-page 根上)
- `window.__jc_sending__` 仍是核心 sending 信号,保持不变
- 这些锚点由匠厂主仓库的以下机制守护:
- `docs/JIANGCHANG_CUSTOM_ANCHORS.md` 中央清单
- 源码内 `JIANGCHANG CUSTOM ANCHOR` 边界注释
- `harness/specs/jiangchang-custom-anchors.spec.ts` E2E 守护测试
- Gitea CI grep 检查 step
### 已知限制
- `send_file()` 暂未实现,抛 `NotImplementedError`
原因:附件上传走 Electron IPC,SDK 需要主仓库提供新 hook。
- `_gateway_state()` 永远返回 `'unknown'`
原因:gateway 状态不再暴露 DOM data-state;本方法已预留未来通过
`window.__jc_gateway_state__` 读取的实现路径。
这两个限制对常规 ask/read 工作流**无影响**,sending 完成判定依靠
`window.__jc_sending__` + `data-sending` + 文本稳定性合流即可。
### 升级影响
- 在 jiangchang v2.0.17(含)以上版本上跑:应正常工作
- 在 jiangchang v2.0.16(及更早含 data-jcid 的版本)上跑:不再兼容
如果需要在旧版本上跑,固定 SDK 版本到 v0.1.x
### 验证清单(集成方建议跑一次)
打开匠厂客户端 → 用 SDK 跑下面 3 步,确认基础流程通:
```python
from jiangchang_desktop_sdk import JiangchangDesktopClient
with JiangchangDesktopClient() as c:
c.ensure_app_running()
c.wait_gateway_ready()
c.new_task()
answer = c.ask("你好,请回复确认 SDK 可用")
assert answer, f"Empty answer: {answer!r}"
print("OK:", answer[:200])
```