init: story-skill - standardized novel continuation agent workflow
- Agent identity + mandatory workflow (AGENTS.md) - Style continuation skill (SKILL.md) - 5 reference file templates (characters/world_building/timeline/style/plot_state) - Batch reading protocol with sequential sub-agents + spot-check review - Bridge script for Gateway API calls with auto novel copy - METHODOLOGY.md explaining the three pillars - README.md with installation guide
This commit is contained in:
50
bridge/SKILL.md
Normal file
50
bridge/SKILL.md
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
name: novelist-bridge
|
||||
description: 调用小说家 agent 进行小说风格续写。自动复制小说原文到 novelist 工作区。
|
||||
---
|
||||
|
||||
# Novelist Bridge
|
||||
|
||||
通过 Gateway API 同步调用 `novelist` agent 进行小说创作。
|
||||
**自动将小说原文复制到 novelist 工作区**,让 novelist 能在本地直接读取原文正文,而不是仅靠标题推断。
|
||||
|
||||
## 脚本位置
|
||||
|
||||
`scripts/ask_novelist.py`
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
# 基本用法(自动查找默认小说文件)
|
||||
python scripts/ask_novelist.py "续写请求内容"
|
||||
|
||||
# 指定小说文件路径
|
||||
python scripts/ask_novelist.py --novel /path/to/novel.txt "续写请求内容"
|
||||
|
||||
# 从 stdin 读取
|
||||
echo "续写内容" | python scripts/ask_novelist.py
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
修改 `ask_novelist.py` 顶部的常量:
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `GATEWAY_URL` | Gateway API 地址 |
|
||||
| `GATEWAY_TOKEN` | Gateway API Token |
|
||||
| `NOVELIST_MODEL` | novelist agent model ID |
|
||||
| `NOVELIST_WORKSPACE` | novelist 工作区路径 |
|
||||
| `NOVEL_TARGET_NAME` | 复制到工作区后的文件名 |
|
||||
|
||||
## 调用链路
|
||||
|
||||
```
|
||||
caller agent → ask_novelist.py → 复制小说到 novelist 工作区 → Gateway API → novelist agent → 回复
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 超时默认 180 秒
|
||||
- 如果 novelist 正在忙,会排队等待
|
||||
- 脚本自动跳过已存在的相同文件(按文件大小比对),减少磁盘写操作
|
||||
127
bridge/ask_novelist.py
Normal file
127
bridge/ask_novelist.py
Normal file
@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bridge script: caller agent → novelist agent (via Gateway API)
|
||||
|
||||
Automatically copies the novel source text to the novelist's workspace so that
|
||||
the novelist agent can read the actual body text locally, not just chapter titles.
|
||||
Then sends the request via Gateway API synchronously.
|
||||
|
||||
Usage:
|
||||
python ask_novelist.py "continuation request"
|
||||
python ask_novelist.py --novel /path/to/novel.txt "continuation request"
|
||||
cat content.txt | python ask_novelist.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import requests
|
||||
|
||||
# Config — customize these for your setup
|
||||
GATEWAY_URL = "http://127.0.0.1:18789/v1/chat/completions"
|
||||
GATEWAY_TOKEN = "<your-gateway-token>"
|
||||
NOVELIST_MODEL = "openclaw/novelist"
|
||||
NOVELIST_WORKSPACE = os.environ.get("NOVELIST_WORKSPACE", "/home/program/novelist-workspace")
|
||||
NOVEL_TARGET_NAME = "novel_source.txt"
|
||||
|
||||
|
||||
def ensure_novel_in_workspace(novel_path: str = None) -> str | None:
|
||||
"""Copy the novel file to the novelist's workspace for local reading."""
|
||||
if not novel_path or not os.path.isfile(novel_path):
|
||||
# Try common default paths
|
||||
candidates = [
|
||||
"/home/program/wechat-workspace/穿越未来之男人不好当.txt",
|
||||
"./novel.txt",
|
||||
os.path.expanduser("~/novel.txt"),
|
||||
]
|
||||
for c in candidates:
|
||||
if os.path.isfile(c):
|
||||
novel_path = c
|
||||
break
|
||||
else:
|
||||
return None
|
||||
|
||||
target = os.path.join(NOVELIST_WORKSPACE, NOVEL_TARGET_NAME)
|
||||
try:
|
||||
if not os.path.isfile(target) or os.path.getsize(target) != os.path.getsize(novel_path):
|
||||
shutil.copy2(novel_path, target)
|
||||
print(f"[桥接] 已复制小说到 {target}", file=sys.stderr)
|
||||
return target
|
||||
except Exception as e:
|
||||
print(f"[警告] 复制小说文件失败: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
def ask(question: str, timeout: int = 180) -> str:
|
||||
"""Synchronous Gateway API call to novelist."""
|
||||
payload = {
|
||||
"model": NOVELIST_MODEL,
|
||||
"messages": [{"role": "user", "content": question}],
|
||||
"stream": False,
|
||||
}
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {GATEWAY_TOKEN}",
|
||||
}
|
||||
try:
|
||||
resp = requests.post(GATEWAY_URL, json=payload, headers=headers, timeout=timeout)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
choices = data.get("choices", [])
|
||||
if choices:
|
||||
return choices[0].get("message", {}).get("content", "")
|
||||
return "[novelist] 返回为空"
|
||||
return f"[novelist] 请求失败({resp.status_code}): {resp.text[:200]}"
|
||||
except requests.exceptions.Timeout:
|
||||
return "[novelist] 超时,请稍后重试"
|
||||
except requests.exceptions.ConnectionError:
|
||||
return "[novelist] 连接失败,请检查 Gateway 服务状态"
|
||||
except Exception as e:
|
||||
return f"[novelist] 请求异常: {str(e)}"
|
||||
|
||||
|
||||
def build_prompt(novel_path: str | None, user_question: str) -> str:
|
||||
"""Build the prompt sent to novelist. Includes novel file path hint."""
|
||||
if novel_path:
|
||||
return (
|
||||
f"小说原文已放在你的工作区,路径为:{novel_path}\n\n"
|
||||
f"请先阅读原文正文(不要只看标题),完成要求。\n\n"
|
||||
f"要求如下:\n{user_question}"
|
||||
)
|
||||
return user_question
|
||||
|
||||
|
||||
def main():
|
||||
novel_path = None
|
||||
args = sys.argv[1:]
|
||||
|
||||
if args and args[0] == "--novel":
|
||||
if len(args) >= 2:
|
||||
novel_path = args[1]
|
||||
question = " ".join(args[2:]) if len(args) > 2 else ""
|
||||
else:
|
||||
print("错误:--novel 需要提供文件路径", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif args:
|
||||
question = " ".join(args)
|
||||
|
||||
if not question:
|
||||
question = sys.stdin.read().strip()
|
||||
|
||||
if not question:
|
||||
print("错误:未提供请求内容", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
copied_path = ensure_novel_in_workspace(novel_path)
|
||||
if copied_path:
|
||||
novel_file = os.path.join(NOVELIST_WORKSPACE, NOVEL_TARGET_NAME)
|
||||
else:
|
||||
novel_file = None
|
||||
|
||||
prompt = build_prompt(novel_file, question)
|
||||
result = ask(prompt)
|
||||
print(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user