Founder-HongYun Editor Automation
方正鸿云学术出版平台自动化技能。使用 browser 工具处理登录和页面交互,API 调用使用 browser.evaluate() 执行。触发关键词:登录方正鸿云、切换刊物、自动催修、自动催审、催审第 X 条、鸿云任务提醒、自动填写送审单、自动注册 DOI、获取登录 Cookie、调用获取刊物信息接口、调用获取发布站点接口、调用获取刊期列表接口、调用获取刊期论文列表接口、调用 DOI 注册接口、将文章{标题}发布到微信公众号。
安装 / 下载方式
TotalClaw CLI推荐
totalclaw install clawskills:behurry~founder-hy-editor-browsercURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/clawskills%3Abehurry~founder-hy-editor-browser/file -o founder-hy-editor-browser.mdGit 仓库获取源码
git clone https://github.com/openclaw/skills/commit/21f8768c06c05be716f6f07e55d71d0d64f583d1# 方正鸿云编辑器浏览器自动化技能
## 环境变量配置(可选)
**以下环境变量均为可选**,如未设置,技能会在需要时提示用户输入:
### 基础配置
| 变量名 | 必需 | 默认值 | 说明 |
|--------|------|--------|------|
| `FOUNDER_PLATFORM_URL` | ❌ | `http://journal.portal.founderss.cn/` | 平台登录地址 |
### 微信发布功能配置(可选)
如需使用 [将文章{标题}发布到微信公众号] 功能,需要配置微信公众号凭证:
| 变量名 | 必需 | 说明 |
|--------|------|------|
| `WECHAT_APP_ID` | ⚠️ 微信发布必需 | 微信公众号 AppID |
| `WECHAT_APP_SECRET` | ⚠️ 微信发布必需 | 微信公众号 AppSecret |
**获取方式**:
1. 登录微信公众平台 (https://mp.weixin.qq.com/)
2. 进入 开发 → 基本配置
3. 获取开发者 ID (AppID) 和 开发者密码 (AppSecret)
**配置方式**(任选其一):
1. **系统环境变量**:
```bash
export FOUNDER_PLATFORM_URL="http://journal.portal.founderss.cn/"
export WECHAT_APP_ID="your_appid"
export WECHAT_APP_SECRET="your_secret"
```
2. **使用时输入(推荐)** - 不设置环境变量,首次使用时手动输入
3. **使用默认值** - 不设置环境变量,使用默认平台地址
---
## 核心规则(重要)
### 规则 1:使用 browser.evaluate() 执行 API 调用
**所有 API 接口调用使用 `browser.evaluate()` 在浏览器上下文中执行**,利用浏览器会话的认证状态,避免 Cookie 失效问题。
| 操作 | 正确方式 |
|------|---------|
| 调用获取刊物信息接口 | `browser.evaluate()` 执行 `fetch()` |
| 调用获取发布站点接口 | `browser.evaluate()` 执行 `fetch()` |
| 调用获取刊期列表接口 | `browser.evaluate()` 执行 `fetch()` |
| 调用获取刊期论文列表接口 | `browser.evaluate()` 执行 `fetch()` |
| 调用 DOI 注册接口 | `browser.evaluate()` 执行 `fetch()` |
| 搜索稿件接口 | `browser.evaluate()` 执行 `fetch()` |
| 版本管理接口 | `browser.evaluate()` 执行 `fetch()` |
| 微信文件接口 | `browser.evaluate()` 执行 `fetch()` |
**调用格式示例**:
```javascript
browser.act(action=act, kind=evaluate, fn="(async () => { const r = await fetch('/api/endpoint', { headers: { 'orgcode': org_code } }); return r.json(); })()")
```
### 规则 2:Cookie 复用机制
**所有需要登录的功能,必须先检查会话中是否存在有效的 `founder_cookie`**:
```
开始功能
↓
检查会话变量 founder_cookie
↓
├─ 存在且有效 → 直接使用
└─ 为空或失效 → 执行 [获取登录 Cookie] → 存储到会话
```
**Cookie 有效性检查**:
- 会话变量为空 → 需要重新获取
- API 调用返回登录重定向(302 到 `/oauth/authorize`)→ Cookie 失效,需要重新获取
**会话变量说明**:
- `founder_cookie` 存储在当前会话的临时内存中
- 会话结束后自动清除
### 规则 3:API 调用范围限制(安全)
**`browser.evaluate()` 仅调用方正鸿云平台的官方 API 接口**,无任意 URL 访问行为:
| 特性 | 说明 |
|------|------|
| **API 域名** | 由环境变量 `FOUNDER_PLATFORM_URL` 决定,默认 `journal.portal.founderss.cn` |
| **域名验证** | 所有方正鸿云平台 API 请求的目标域名必须与 `FOUNDER_PLATFORM_URL` 同源 |
| **API 路径** | 不限具体路径,在平台域名下按需调用 |
| **第三方服务** | 仅访问 `api.weixin.qq.com`(微信发布功能必需) |
| **无任意 URL** | 不支持用户传入任意 URL 进行请求 |
| **请求透明** | 所有 API 调用在浏览器开发者工具中可见 |
**安全保证**:
- ✅ 方正鸿云平台 API 调用在浏览器上下文中执行,受同源策略限制
- ✅ 微信 API 调用通过 `exec + curl` 执行,目标固定为 `api.weixin.qq.com`
- ✅ 不调用任何未声明的外部服务
- ✅ 所有网络请求可通过浏览器开发者工具审计
### 规则 4:API 调用方式说明
| API 类型 | 调用方式 | 权限需求 | 目标域名 |
|---------|---------|---------|---------|
| 方正鸿云平台 API | `browser.evaluate()` + `fetch()` | `browser` | `FOUNDER_PLATFORM_URL` |
| 微信公众号 API | `exec` + `curl` | `exec` | `api.weixin.qq.com` |
**说明**:
- ✅ 方正鸿云平台 API 在浏览器上下文中执行,复用登录会话(Cookie)
- ✅ 微信 API 通过 shell curl 调用,目标固定为 `api.weixin.qq.com`
- ❌ 不调用任何未声明的外部服务
- ❌ 不接受用户传入的任意 URL
---
## 🔒 安全风险提示
### Cookie 处理说明
| 项目 | 说明 |
|------|------|
| **Cookie 来源** | 用户登录方正鸿云平台后生成 |
| **存储位置** | 仅存储在会话内存中(临时变量 `founder_cookie`) |
| **持久化** | ❌ 不写入任何文件或数据库 |
| **会话结束** | ✅ 自动清除 |
| **用户审计** | 可通过浏览器开发者工具 Network 标签查看 |
### 权限使用说明
| 权限 | 用途 | 范围限制 |
|------|------|---------|
| `browser` | 登录、页面交互、方正鸿云平台 API 调用 | 仅限 `FOUNDER_PLATFORM_URL` 域名 |
| `exec` | 微信公众号 API 调用(curl) | 仅限 `api.weixin.qq.com` |
### 用户控制
- ✅ 用户可随时查看会话变量内容
- ✅ 用户可通过浏览器开发者工具审计所有网络请求
- ✅ 用户可选择不使用微信发布功能(无需 exec)
- ✅ 所有 API 调用在 SKILL.md 中明确声明
---
## ⚠️ 安全模型说明
### 信任模型
本技能是 **instruction-only** 类型,安全约束基于以下机制:
| 约束类型 | 强制方式 | 说明 |
|---------|---------|------|
| **同源策略** | ✅ 浏览器强制 | 无法访问跨域资源(CORS 限制) |
| **API 调用范围** | ⚠️ 程序性约束 | 依赖技能代码严格遵守声明 |
| **Cookie 使用** | ⚠️ 程序性约束 | 依赖技能代码不滥用 |
| **权限使用** | ⚠️ 程序性约束 | 依赖技能代码按声明用途使用 |
### 技术限制(浏览器强制)
以下限制由浏览器自动执行,**无法被技能代码绕过**:
| 限制 | 说明 | 强制机制 |
|------|------|---------|
| **跨域请求** | 无法访问未声明 CORS 的外部 API | CORS 策略 |
| **跨域 Cookie** | 无法读取其他网站的 Cookie | 同源策略 |
| **localStorage** | 无法访问其他域名的 localStorage | 同源策略 |
| **文件系统** | 无法直接访问用户文件系统 | 浏览器沙箱 |
### 程序性约束(依赖代码自律)
以下约束依赖技能代码严格执行,**用户可通过审计验证**:
| 约束 | 验证方法 |
|------|---------|
| 仅访问声明的域名 | 浏览器 Network 标签审计 |
| 仅调用声明的 API | 代码审查 + Network 标签 |
| Cookie 不持久化 | 检查会话变量 + 文件系统 |
| 权限不滥用 | 代码审查 + 运行时审计 |
### 用户如何验证
#### 1. 代码审查(使用前)
- ✅ 技能代码完全开源,可在 ClawHub 查看
- ✅ 所有 API 调用在 SKILL.md 中明确声明
- ✅ 权限声明在 `_meta.json` 中公开
#### 2. 运行时审计(使用中)
- ✅ 打开浏览器开发者工具(F12)
- ✅ 查看 **Network** 标签 - 所有请求可见
- ✅ 查看 **Application** 标签 - Cookie 和存储可见
#### 3. 会话变量检查(随时)
- ✅ 用户可随时要求查看会话变量内容
- ✅ 用户可要求检查 `founder_cookie` 值
- ✅ 会话结束后变量自动清除
### 违规的技术后果
如果技能代码尝试违反声明:
| 违规行为 | 技术后果 |
|---------|---------|
| 访问未声明的域名 | CORS 阻止请求 |
| 读取其他网站 Cookie | 同源策略阻止 |
| 持久化存储 Cookie | 用户可在文件系统检查 |
| 调用未声明的 API | Network 标签可见 |
### 开源承诺
- ✅ 技能代码完全开源
- ✅ 无隐藏功能或后门
- ✅ 欢迎安全社区审查
- ✅ 发现问题请报告
---
## 触发关键词与操作流程
### 1. 登录方正鸿云
**触发词**: `登录方正鸿云`
**操作步骤**:
1. 使用 `browser` 工具打开平台地址
2. 等待登录页面加载完成
3. 等待用户输入用户名和密码
4. 用户完成登录后,等待进入主页面
5. 执行 [获取登录 Cookie] 保存到会话变量
6. 向用户反馈登录完成
---
### 2. 切换刊物
**触发词**: `切换刊物` 或 `切换刊物 {刊物名称}`
**功能说明**: 获取用户有权限的刊物列表,支持带参数切换刊物,切换后自动初始化新刊物并刷新 Cookie
**操作步骤**:
#### 第一步:检查 Cookie
- 检查会话变量 `founder_cookie` 是否为空
- 若为空 → 执行 [登录方正鸿云]
#### 第二步:调用获取刊物信息接口
1. **通过 browser.evaluate 调用**:
```javascript
browser.act(action=act, kind=evaluate, fn="(async () => { const r = await fetch('/je-api/journal-edit-reviewer/v1/review/journal?timestamps=' + Date.now()); return r.json().then(d => JSON.stringify(d)); })()")
```
2. **解析返回的 JSON 数据**:
- 提取 `data.magazineId` → `z_journal_id`
- 提取 `data.orgCode` → `org_code`
- 存入会话变量 `z_journal_id` 和 `org_code`
- 同时更新 `journal_id` 和 `journal_name`
#### 第三步:选择刊物
- **如果用户指定了刊物名称** → 模糊匹配刊物列表
- **如果未指定** → 提示用户输入刊物名称
- **如果匹配成功** → 提示:"✅ 匹配成功"
#### 第四步:新刊初始化
- 调用新刊初始化接口
- 返回 `status=0` → 初始化成功
#### 第五步:重新获取登录 Cookie
- 从浏览器会话提取最新 Cookie
- 更新会话变量 `founder_cookie`
#### 向用户反馈
```
✅ 切换刊物完成
当前刊物:{journal_name}
刊物 ID: {journal_id}
机构代码:{org_code}
```
---
### 2.1 将文章{标题}发布到微信公众号
**触发词**: `将文章 {文章标题} 发布到微信公众号`
**功能说明**: 根据文章标题搜索稿件,获取微信格式文件,调用微信 API 发布到微信公众号
**操作步骤**:
#### 第一步:检查 Cookie
- 检查会话变量 `founder_cookie` 是否为空
- 若为空 → 执行 [登录方正鸿云]
#### 第二步:执行 [调用获取刊物信息接口]
1. **调用技能已有功能** [调用获取刊物信息接口]
2. **通过 browser.evaluate 调用**:
```javascript
browser.act(action=act, kind=evaluate, fn="(async () => { const r = await fetch('/je-api/journal-edit-reviewer/v1/review/journal?timestamps=' + Date.now()); return r.json().then(d => JSON.stringify(d)); })()")
```
3. **解析返回的 JSON 数据**:
- 提取 `data.magazineId` → `z_journal_id`
- 提取 `data.orgCode` → `org_code`
- 存入会话变量 `z_journal_id` 和 `org_code`
- 同时更新 `journal_id` 和 `journal_name`(技能已有功能会自动更新)
#### 第三步:调用搜索稿件接口(POST)
1. **通过 browser.evaluate 调用**(保持浏览器会话):
```javascript
browser.act(action=act, kind=evaluate, fn="(async () => { const r = await fetch('/article/portalDocList.do', { method: 'POST', headers: { 'Content-Type': 'application/json; charset=UTF-8', 'orgcode': org_code }, body: JSON.stringify({ magazineId: z_journal_id, sectionState: '', currentState: 0, currentHandler: 2, isPriority: null, collateTimes: '', magazineIssueId: '', preIssue: '', editorId: '', keyWord: article_title, pageNumer: 1, sortType: '', sortField: '', scopeone: 1, portal: 'portal', preIssueShow: '1' }) }); return r.json().then(d => JSON.stringify(d)); })()")
```
**注意**:
- `z_journal_id`、`org_code` 和 `article_title` 需要从会话变量传入
- 请求头必须包含 `orgcode`,值为会话变量 `org_code`
2. **解析返回的 JSON 数据**:
- 提取 `data.rows` 数组(稿件列表)
- **如果 `data.rows` 为空或长度为 0** → 提示:"❌ 搜索的稿件为空",结束流程
- 获取第一条稿件的 `id` → `article_id`
- 存入会话变量 `article_id`
**返回 JSON 样例**:
```json
{
"status": 0,
"message": "成功",
"data": {
"total": 2,
"pageCount": 1,
"pageNumber": 1,
"pageSize": "50",
"rows": [
{ "id": "29533" },
{ "id": "29532" }
]
}
}
```
#### 第四步:循环版本记录查找微信文件 ID
1. **通过 browser.evaluate 调用**(保持浏览器会话):
```javascript
browser.act(action=act, kind=evaluate, fn="(async () => { const r = await fetch('/article/versionManage.do?id=' + article_id + '&userId=851&pageNumer=1&flowNode=f26_59&portal=portal&isyw=f