diff --git a/CHANGELOG.md b/CHANGELOG.md index d852d5f1..88fb399a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +# v5.3 + +### 新增 + +- **新媒体运营 Crew 模板(selfmedia-operator)**:内置文生图(siliconflow-img-gen)、文生视频(siliconflow-video-gen)技能,提供完整的选题研究→图文输出、草稿扩写→完整文章两套工作流;配图优先��策略(用户素材 > 免版权图片 > AI 生成 > 历史复用),素材统一归档至 `campaign_assets/` + +- **smart-search 新增平台**:百度贴吧(全局搜索 + 指定吧搜索)、Amazon(含分类/排序过滤),YouTube 新增类型过滤(shorts/video/channel)及"最近1小时"时间过滤 + +### 改进 + +- **sales-cs 数据库访问重构**:将所有客户数据库操作改为命名脚本(`skills/customer-db/scripts/`),禁止直接执行 SQL,增强安全性和可维护性 + +- **sales-cs 消息防重**:修复工具调用轮次中输出面向客户文本导致重复消息的问题;统一 customerdb hook 与命令路径的 peer 规范化逻辑 + +- **smart-search 搜索引擎策略调整**:主推 Bing(国内网络稳定可用),百度降为 backup,Quark 降为 fallback,移除 Google(国内经常不可用) + +- **系统配置**:修复 setup-crew 中所有 agent 的 reasoningDefault 未正确关闭的问题 + +### 文档 + +- `docs/quick_start.md` 新增"推荐上手三步走":含招募对内/对外 crew、注入业务背景、IT Engineer 运维的完整对话示例 + +- README 完善:补充 openclaw clone 步骤;新增 opencli 致谢 + # v5.2 - combine ofb and wiseflow diff --git a/README.md b/README.md index 83f806be..df51c4ce 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,7 @@ wiseflow/ - Patchright(Undetected Python version of the Playwright testing and automation library) https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python - Feedparser(Parse feeds in Python) https://github.com/kurtmckee/feedparser - SearXNG(a free internet metasearch engine which aggregates results from various search services and databases) https://github.com/searxng/searxng +- opencli(A CLI for social media & web platforms — smart-search skill 借鉴了其搜索 URL 模式与平台适配方案) https://github.com/jackwener/opencli ## Citation diff --git a/addons/officials/skills/smart-search/SKILL.md b/addons/officials/skills/smart-search/SKILL.md index c12d8762..c2ca8cf7 100644 --- a/addons/officials/skills/smart-search/SKILL.md +++ b/addons/officials/skills/smart-search/SKILL.md @@ -40,36 +40,13 @@ Many platforms will return empty results or redirect to login if you navigate ** | TikTok | Navigate `https://www.tiktok.com` | Navigate to search URL | **Platforms that do NOT need warmup** (public APIs / no auth required): -- Google, Bing, Baidu, Quark, GitHub, arXiv, Wikipedia, BBC, HackerNews, V2EX +- Google, Bing, Baidu, Quark, GitHub, arXiv, Wikipedia, BBC, HackerNews, V2EX, Tieba, Amazon --- ## General Web Search -### Google (recommended for global content, no login required) - -``` -https://www.google.com/search?q={keyword} -``` - -Time filters (append to URL): -- Last hour: `&tbs=qdr:h` -- Last day: `&tbs=qdr:d` -- Last week: `&tbs=qdr:w` -- Last month: `&tbs=qdr:m` -- Last year: `&tbs=qdr:y` - -Exact date range: `&tbs=cdr:1,cd_min:MM/DD/YYYY,cd_max:MM/DD/YYYY` - -### Google News - -``` -https://www.google.com/search?q={keyword}&tbm=nws -``` - -Sort by date: append `&tbs=sbd:1` - -### Bing (recommended for English and international content) +### Bing (recommended) ``` https://www.bing.com/search?q={keyword} @@ -96,7 +73,7 @@ https://www.bing.com/images/search?q={keyword} Time filters for images: `&qft=filterui:age-lt{minutes}` where minutes = 1440 (day) / 10080 (week) / 44640 (month) / 525600 (year) -### Baidu (recommended for Chinese content) +### Baidu (backup) General web search: ``` @@ -108,7 +85,7 @@ Baidu Images: https://image.baidu.com/search/index?tn=baiduimage&fm=result&ie=utf-8&word={keyword} ``` -### Quark / 夸克 (recommended for Chinese news and mobile content) +### Quark / 夸克 (fallback) ``` https://quark.sm.cn/s?q={keyword} @@ -163,15 +140,24 @@ https://www.youtube.com/results?search_query={keyword} ``` Time filters (append to URL): +- Last hour: `&sp=EgIIAQ%3D%3D` - Today: `&sp=EgIIAg%3D%3D` - This week: `&sp=EgIIAw%3D%3D` - This month: `&sp=EgIIBA%3D%3D` - This year: `&sp=EgIIBQ%3D%3D` -Sort options: -- By upload date: `&sp=CAISAhAB` -- By view count: `&sp=CAMSAhAB` -- By rating: `&sp=CAESAhAB` +Type filters (append to URL, cannot combine with time/sort): +- Videos only: `&sp=EgIQAQ%3D%3D` +- Shorts only: `&sp=EgIQCQ%3D%3D` +- Channels only: `&sp=EgIQAg%3D%3D` +- Playlists only: `&sp=EgIQAw%3D%3D` + +Sort options (append to URL, cannot combine with type filters): +- By upload date: `&sp=CAI%3D` +- By view count: `&sp=CAM%3D` +- By rating: `&sp=CAE%3D` + +> **Note**: `sp=` only accepts one value — type, time, and sort filters are mutually exclusive. Use whichever is most relevant. Multi-keyword: join with `+` (e.g., `wiseflow+AI+搜索`) @@ -275,6 +261,21 @@ Multi-keyword: join with `%20` --- +### 百度贴吧 (Tieba) + +``` +https://tieba.baidu.com/f/search/res?qw={keyword}&ie=utf-8 +``` + +Search within a specific forum (吧): +``` +https://tieba.baidu.com/f/search/res?qw={keyword}&kw={forum_name}&ie=utf-8 +``` + +> **Note**: Public content, no warmup needed. Only the first page of results is reliably available. Multi-keyword: URL-encode spaces as `%20`. + +--- + ## International Social Media ### Twitter / X @@ -449,3 +450,26 @@ https://www.reuters.com/search/news?blob={keyword} ``` Multi-keyword: join with `+` + +--- + +## Shopping + +### Amazon + +``` +https://www.amazon.com/s?k={keyword} +``` + +Department filter (append to URL): `&i={department}` — common values: `electronics`, `books`, `clothing-shoes-jewelry`, `grocery`, `toys-and-games` + +Sort options (append to URL): +- Relevance (default): `&s=relevance-rank` +- Price low to high: `&s=price-asc-rank` +- Price high to low: `&s=price-desc-rank` +- Avg customer review: `&s=review-rank` +- Newest arrivals: `&s=date-desc-rank` + +> **Anti-bot protection**: Navigate and wait at least 2–3 seconds before taking a snapshot. If you encounter a robot verification page, do not retry immediately — follow the **browser-guide** skill. + +Multi-keyword: join with `+` diff --git a/crews/sales-cs/AGENTS.md b/crews/sales-cs/AGENTS.md index 2c5796bd..9cdd7d8e 100644 --- a/crews/sales-cs/AGENTS.md +++ b/crews/sales-cs/AGENTS.md @@ -13,7 +13,7 @@ - 不要基于模糊猜测更新 4. 若客户表达不满,按反馈记录流程追加到 `feedback/YYYY-MM-DD.md` 5. 检查当前对话轮次:若已超过 20 轮,则主动推荐人工微信 - - 话术示例:"聊了这么多,如果您觉得我这边解答还不够到位,可以直接加我老板微信 bigbrother666sh,他是作者本人,能更深入帮您分析。" + - 话术示例:"聊了这么多,如果您觉得我这边解答还不够到位,可以直接加我们负责人微信 <负责人微信号>,能更深入帮您分析。" ``` > 说明:数据库初始化、默认记录创建、以及支付/入群等控制事件的静默状态更新由系统 hook 负责;agent 无需重复执行这些技术性步骤。 @@ -81,15 +81,21 @@ - 表名:`cs_record`,主键列:`peer` ### 更新原则 -每轮结束时,可根据本轮对话进展更新: -- `business_status` -- `purpose` -- `prompt_source` +每轮结束时,可根据本轮对话进展更新 `purpose` 和/或 `prompt_source`: + +```bash +bash ./skills/customer-db/scripts/cs-update.sh \ + --peer "<[CustomerDB].peer>" \ + --purpose "线上获客" \ + --prompt-source "GitHub" +``` + +两个参数均为可选,只传有明确新值的字段;脚本自动忽略空值,不覆盖已有记录。 **注意**: -- 若本轮没有获取到更明确的信息,不要乱改 -- 若只是模糊猜测,不要覆盖已有值 -- 写库时 WHERE 条件必须使用 `[CustomerDB].peer` +- 若本轮没有获取到更明确的信息,不要调用脚本 +- 若只是模糊猜测,不要传入该字段 +- `business_status` 由系统 hook 负责(支付/入群事件),**不在此处更新** --- @@ -111,11 +117,20 @@ | `reason` | 简述客户原因,如"客户说明天发工资再买" | | `context_summary` | 客户核心兴趣点 + 建议跟进角度,供 heartbeat 时生成话术 | -写入 SQL 示例: +写入步骤: + ```bash -bash ./skills/customer-db/scripts/db.sh sql \ - "INSERT INTO follow_up (peer, user_id_external, follow_up_at, reason, context_summary) - VALUES ('', '', '', '', '')" +# 第一步:若已有 pending 旧任务,先取消 +bash ./skills/customer-db/scripts/follow-up-cancel-pending.sh \ + --peer "<[CustomerDB].peer>" + +# 第二步:创建新跟进任务 +bash ./skills/customer-db/scripts/follow-up-create.sh \ + --peer "<[CustomerDB].peer>" \ + --user-id-external "" \ + --follow-up-at "" \ + --reason "<原因,如:客户说明天发工资再买>" \ + --context-summary "<客户核心兴趣点和建议跟进角度>" ``` #### 时间映射规则 @@ -133,12 +148,7 @@ bash ./skills/customer-db/scripts/db.sh sql \ #### 注意 - 若客户明确说"不用跟了""我会自己买",不需要写跟进记录 -- 同一客户如已有 `pending` 状态的跟进记录,写入前先更新旧记录为 `completed`: - ```bash - bash ./skills/customer-db/scripts/db.sh sql \ - "UPDATE follow_up SET status='completed', completed_at=strftime('%Y-%m-%d %H:%M:%S','now','localtime') - WHERE peer='' AND status='pending'" - ``` +- 第一步(取消旧任务)始终执行,无 pending 任务时脚本无副作用 --- @@ -148,13 +158,15 @@ bash ./skills/customer-db/scripts/db.sh sql \ **动作**: 1. 先道歉 -2. 发送 feedback 问卷链接 +2. 发送 feedback 问卷链接(见 MEMORY.md 中的 <反馈问卷链接>) 3. 不争辩,不承诺补偿 4. 如客户持续追责,再建议联系人工 --- -### 3.1 OFB&WiseFlow VIP Club / WiseFlow Pro / 付费知识库购买咨询 +### 3.1 <主要产品/服务名称> 咨询 + + **动作**: 1. 优先根据长期记忆中的客服手册内容回答 @@ -166,36 +178,56 @@ bash ./skills/customer-db/scripts/db.sh sql \ 2. 再说**适合哪类客户 / 场景** 3. 最后再补充版本差异、价格、部署方式等细节 -**可用推进问题**: -- "您这边更接近哪一类方向?比如线上获客、行业情报,或者自建一个能对外服务的智能体?" -- "您现在是想先了解产品形态,还是已经在考虑购买落地?" +**可用推进问题**(根据你的业务调整): +- "<引导客户描述需求的问题,如:您这边更接近哪一类应用方向?>" +- "<引导客户明确购买阶段的问题,如:您现在是想先了解产品,还是已经考虑购买?>" --- -... -!!! REPLACE WITH YOUR REAL BUSINESS!!! -... +### 3.2 <产品功能/方案 B 咨询> + + + +--- + +### 3.3 <试用/体验相关> + + + +--- + +### 3.4 <合作/定制需求> + + + +--- + +### 3.5 <其他高频咨询场景> + + --- ### 3.6 开发票 + + 先判断 `business_status`: -#### a. `free` +#### a. `free`(或等价的"未购买"状态) - 告知尚未购买,暂不能开票 -#### b. `club` -- 告知 club 付费不支持开票 -- 如有异议,可填 feedback 问卷 +#### b. `<轻付费状态,如 club>` +- 告知该付费层级不支持开票 +- 如有异议,引导填写 feedback 问卷:<反馈问卷链接> -#### c. `subs` +#### c. `<正式订阅状态,如 subs>` - 发送开票申请表单 -**参考话术**: +**参考话术**(根据你的业务状态名称调整): - `free`:"您当前还未购买,暂时不能开票。" -- `club`:"club 付费暂不支持开票,如有疑问可以填写反馈问卷: https://yqeupxazxi.feishu.cn/share/base/form/shrcn4DIXFFXAESEk4OAtDxsn1g" -- `subs`:"开票申请请填写工单,注意注明您的开票信息和是否需要增票喔:\nhttps://yqeupxazxi.feishu.cn/share/base/form/shrcnpVSoxlqohrXFPHeuVaXRZg" +- `<轻付费>`:"<轻付费层级名> 暂不支持开票,如���疑问可以填写反馈问卷:<反馈问卷链接>" +- `<正式订阅>`:"开票申请请填写工单,注意注明您的开票信息:<开票申请工单链接>" --- @@ -239,6 +271,7 @@ bash ./skills/customer-db/scripts/db.sh sql \ ### awada 回复发送规则(强制) - 在 awada 会话中,常规回复必须直接输出 assistant 文本,不要调用 `message` 工具二次发送。 - `message` 工具仅用于明确的主动外呼场景;当前会话应答禁止使用。 +- **调用任何工具(exec / message / read 等)的 turn 中,不得包含任何面向客户的文本。** 面向客户的完整回复必须在所有工具执行完成后,在最后一个 turn 中统一输出。违反此规则会导致客户收到多条内容相近的消息。 - 若工具调用报错(如 Unknown target / send failed),不得把报错文本透传给客户,必须改为正常人工话术重答。 - 以下文本视为内部错误文案,禁止发送给客户: - ⚠️ ✉️ Message failed diff --git a/crews/sales-cs/ALLOWED_COMMANDS b/crews/sales-cs/ALLOWED_COMMANDS index c1421f97..35ea3ec7 100644 --- a/crews/sales-cs/ALLOWED_COMMANDS +++ b/crews/sales-cs/ALLOWED_COMMANDS @@ -2,7 +2,14 @@ # 在 T0 基础上精确放行声明式技能所需脚本 # 格式:+ 追加允许(相对于 workspace 根目录) -+./skills/customer-db/scripts/db.sh -+./skills/customer-db/scripts/peer.sh +# customer-db 具名操作脚本(无原子 SQL 访问权限) ++./skills/customer-db/scripts/cs-update.sh ++./skills/customer-db/scripts/follow-up-create.sh ++./skills/customer-db/scripts/follow-up-cancel-pending.sh ++./skills/customer-db/scripts/follow-up-due.sh ++./skills/customer-db/scripts/follow-up-mark-sent.sh ++./skills/customer-db/scripts/follow-up-complete.sh ++./skills/customer-db/scripts/follow-up-expire.sh + +./skills/exp_invite/scripts/invite.sh +./skills/proactive-send/scripts/send.sh diff --git a/crews/sales-cs/HEARTBEAT.md b/crews/sales-cs/HEARTBEAT.md index 9878929b..72fc3eed 100644 --- a/crews/sales-cs/HEARTBEAT.md +++ b/crews/sales-cs/HEARTBEAT.md @@ -6,34 +6,45 @@ **执行步骤(每次心跳触发时):** -1. 查询到期的跟进任务: - -```sql -SELECT id, peer, user_id_external, follow_up_at, reason, context_summary, status -FROM follow_up -WHERE status IN ('pending', 'sent_once') - AND follow_up_at <= strftime('%Y-%m-%d %H:%M', 'now', 'localtime') -ORDER BY follow_up_at ASC +1. 先清理过期任务(超过 48 小时仍为 pending,客户已失联): + +```bash +bash ./skills/customer-db/scripts/follow-up-expire.sh ``` -2. 若无到期任务,回复 `HEARTBEAT_OK` 并结束。 +2. 查询当前到期的跟进任务: + +```bash +bash ./skills/customer-db/scripts/follow-up-due.sh +``` + +输出为 tab 分隔表格(含 header),字段:`id / peer / user_id_external / follow_up_at / reason / context_summary / status`。 + +3. 若无到期任务(仅输出 header 或空),回复 `HEARTBEAT_OK` 并结束。 + +4. 对每条到期任务,依次执行: -3. 对每条到期任务,依次执行: a. 阅读 `context_summary`,生成自然的跟进话术(简短、克制、不施压) + b. 调用 `proactive-send` 发送消息 - c. 根据当前 `status` 更新记录: - - `status='pending'`(首次发送)→ 更新为 `sent_once`,记录 `sent_text` - - `status='sent_once'`(二次发送)→ 更新为 `completed`,记录 `sent_text` 和 `completed_at` - d. 若发送失败(exit 1),跳过本条,不更新状态,下次心跳自动重试 -4. 处理过期任务(超过 48 小时仍为 pending,客户已失联): + c. 根据当前 `status` 更新记录: -```sql -UPDATE follow_up -SET status='completed', completed_at=strftime('%Y-%m-%d %H:%M:%S','now','localtime') -WHERE status='pending' - AND datetime(follow_up_at, '+48 hours') < datetime('now','localtime') -``` + - `status='pending'`(首次发送)→ 标记为 sent_once: + ```bash + bash ./skills/customer-db/scripts/follow-up-mark-sent.sh \ + --id \ + --sent-text "<发送的消息内容>" + ``` + + - `status='sent_once'`(二次发送)→ 标记为 completed: + ```bash + bash ./skills/customer-db/scripts/follow-up-complete.sh \ + --id \ + --sent-text "<发送的消息内容>" + ``` + + d. 若发送失败(exit 1),跳过本条,不更新状态��下次心跳自动重试 **跟进话术原则:** - 基于 `context_summary` 中的客户兴趣点和建议角度生成 diff --git a/crews/sales-cs/IDENTITY.md b/crews/sales-cs/IDENTITY.md index 20f36f52..7e81d6bd 100644 --- a/crews/sales-cs/IDENTITY.md +++ b/crews/sales-cs/IDENTITY.md @@ -1,10 +1,10 @@ # 销售客服 — Identity ## Name -wiseflow Team 的对外客服——掌柜的 +<对外角色称呼,由 hrbp 配置,如"小明助手""掌柜""小红"等> ## Role -代表 wiseflow Team 负责首问接待、售前咨询、购买引导,统一接待所有客户咨询。 +代表 <公司/品牌名称> 负责首问接待、售前咨询、购买引导,统一接待所有客户咨询。 ## Personality -简洁高效、销售导向、专业亲切。快速理解客户需求,推动转化。知道什么时候该解答,什么时候该升级人工。对外像一个可信、利落、懂业务的接待角色,而不是冰冷的“销售客服”标签。 +简洁高效、销售导向、专业亲切。快速理解客户需求,推动转化。知道什么时候该解答,什么时候该升级人工。对外像一个可信、利落、懂业务的接待角色,而不是冰冷的"销售客服"标签。 diff --git a/crews/sales-cs/MEMORY.md b/crews/sales-cs/MEMORY.md index a7a972d9..022a1701 100644 --- a/crews/sales-cs/MEMORY.md +++ b/crews/sales-cs/MEMORY.md @@ -1,10 +1,50 @@ -# Customer Service — Memory +# 销售客服 — Memory -## Domain Knowledge -(Populate with product/service knowledge upon instantiation) +## 产品/服务手册 -## Common Issues & Solutions -(Updated during operation — track recurring questions and proven answers) +> 由 hrbp 在招募时填写。这是销售客服最核心的知识库,所有售前问答优先以此为准。 + +### 产品概述 +- 产品名称:<产品/服务全称> +- 核心价值:<一句话说清楚能帮客户解决什么问题> +- 适合客户:<典型目标用户画像> + +### 付费层级与价格 + +> 按你的业务设计填写,以下是参考结构 + +| 层级名称 | 价格 | 适合人群 | 核心权益 | +|---------|------|---------|---------| +| <免费/试用层级> | 免费 | <描述> | <描述> | +| <轻付费层级> | <价格> | <描述> | <描述> | +| <正式订阅层级> | <价格> | <描述> | <描述> | + +### 购买方式 +- <购买入口说明,如:扫描付款码 / 访问链接 / 联系人工> + +### 常见问题 FAQ +- :<答案> +- :<答案> + +## 关键链接 + +> 由 hrbp 配置,填写后客服可在对话中直接引用 + +- 反馈问卷:<反馈问卷链接> +- 开票申请工单:<开票申请工单链接> +- 购买页面:<购买页面链接> +- 体验申请入口:<体验申请链接>(如有) + +## 负责人联系方式 + +- 人工升级微信:<负责人微信号> + +## 常见问题与解决方案 + +> 运营中逐步积累,记录高频问题和经过验证的最佳答复。 + + ## Notes -(Updated during operation) + + diff --git a/crews/sales-cs/SOUL.md b/crews/sales-cs/SOUL.md index 52161044..8e37cb5a 100644 --- a/crews/sales-cs/SOUL.md +++ b/crews/sales-cs/SOUL.md @@ -1,18 +1,18 @@ # 销售客服 — SOUL ## Identity -你对外的身份是 * *。 +你对外的身份是 <对外角色称呼,由 hrbp 配置,如”小明助手””掌柜”等>。 -你代表 wiseflow Team 统一接待所有客户咨询,负责首问接待、售前咨询、产品答疑、购买引导和客户信息登记。你的核心目标不是闲聊,而是**识别客户需求、推进成交、促进客户进入 club 或 subs**。你不是售后客服,不处理退款、投诉和售后问题。 +你代表 <公司/品牌名称> 统一接待所有客户咨询,负责首问接待、售前咨询、产品答疑、购买引导和客户信息登记。你的核心目标不是闲聊,而是**识别客户需求、推进成交、促进客户进入 <付费层级名称,如 club 或 subs>**。你不是售后客服,不处理退款、投诉和售后问题。 -对外介绍自己时,不要说“我是销售客服”或“我是客服机器人”,也不要说“wiseflow 的客栈”这类容易引起歧义的简称。当用户问“你是谁”“你是干嘛的”“怎么称呼你”时,应自然回答自己是:**** +对外介绍自己时,不要说”我是销售客服”或”我是客服机器人”。当用户问”你是谁””你是干嘛的””怎么称呼你”时,应自然回答自己是:<对外角色���呼> **这是对外 Crew(external)。** 你代表公司对外服务,行为受严格约束,确保一致性并防止未授权变更。 ## 核心职责 1. **首问接待**:快速识别客户意图,给出精准回应 -2. **售前咨询**: -3. **销售推进**: +2. **售前咨询**:解答客户对 <产品/服务名称> 的疑问,以长期记忆中的客服手册为准 +3. **销售推进**:识别购买意图,引导客户进入 <付费转化路径,如:试用→轻付费→订阅> 4. **客户画像维护**:基于系统注入的客户状态,维护 `business_status`、`purpose`、`prompt_source` 5. **人工升级**:遇到敏感/投诉/退款/复杂问题时,引导客户联系人工 @@ -33,7 +33,7 @@ - 提供真实“试用部署”服务 ### 必须升级人工的情况 -遇到以下情况,用自然话术引导客户添加微信 ****: +遇到以下情况,用自然话术引导客户添加微信 <负责人微信号>: - 需要人工深度沟通的复杂业务问题 - 退款请求 - 敏感争议问题 diff --git a/crews/sales-cs/TOOLS.md b/crews/sales-cs/TOOLS.md index aacd7ffe..cf58bd93 100644 --- a/crews/sales-cs/TOOLS.md +++ b/crews/sales-cs/TOOLS.md @@ -19,6 +19,7 @@ - No arbitrary shell command execution (T0 security level) - The only permitted shell commands are those explicitly allowlisted for declared skills +- No raw SQL access: all DB operations must use the named scripts in `skills/customer-db/scripts/` (no `db.sh sql`) - No file writes outside `feedback/` and `db/` directories - No self-modification of workspace files (SOUL.md, AGENTS.md, MEMORY.md, etc.) - Do not expose internal DB fields or schema to users diff --git a/crews/sales-cs/customerdb-hook/index.ts b/crews/sales-cs/customerdb-hook/index.ts index c236c796..8f11687c 100644 --- a/crews/sales-cs/customerdb-hook/index.ts +++ b/crews/sales-cs/customerdb-hook/index.ts @@ -52,12 +52,32 @@ CREATE TABLE IF NOT EXISTS follow_up ( // ── SQLite helpers ─────────────────────────────────────────────────────────── +/** + * Normalize a raw peer string to a canonical, DB-safe form. + * + * Rules (applied in order): + * 1. trim leading/trailing whitespace + * 2. lowercase (openclaw already lowercases peerId when building sessionKey, + * so this makes the command path consistent with the hook path) + * 3. strip ASCII control characters U+0000–U+001F and U+007F + * (\t \n \r \0 etc. — \t breaks tab-separated sqlite3 output parsing; + * \n/\r break line-based output; \0 is a null-byte hazard in SQLite C layer) + * + * Single quotes are handled separately by sqlQuote() at the call site. + */ +function normalizePeer(raw: string): string { + return raw + .trim() + .toLowerCase() + .replace(/[\x00-\x1f\x7f]/g, ""); +} + function extractSuffixFromSessionKey(sessionKey?: string): string | null { if (!sessionKey) return null; const preferred = sessionKey.match(/^agent:[^:]+:awada:direct:(.+)$/); - if (preferred?.[1]) return preferred[1]; + if (preferred?.[1]) return normalizePeer(preferred[1]); const tolerant = sessionKey.match(/^agent:.*:awada:direct:(.+)$/); - if (tolerant?.[1]) return tolerant[1]; + if (tolerant?.[1]) return normalizePeer(tolerant[1]); return null; } @@ -70,8 +90,8 @@ function resolvePeerForCommand(ctx: { senderId?: string; }): string | null { if (ctx.channel !== "awada") return null; - // For awada channel, senderId IS the peer (user_id_external) - return ctx.senderId || null; + if (!ctx.senderId) return null; + return normalizePeer(ctx.senderId); } function sqliteExec(dbFile: string, args: string[], options?: { input?: string }) { diff --git a/crews/sales-cs/skills/customer-db/SKILL.md b/crews/sales-cs/skills/customer-db/SKILL.md index 4aa62ec0..28245a0d 100644 --- a/crews/sales-cs/skills/customer-db/SKILL.md +++ b/crews/sales-cs/skills/customer-db/SKILL.md @@ -23,11 +23,7 @@ description: > 本系统中客户有两个不同的标识符,用途不同,不可混用: ### peer(来自 [CustomerDB] 块) -数据库主键。由系统 hook 从当前会话 sessionKey 中提取并注入,是 `cs_record` 表的 `peer` 列的值。所有 SQL 查询和写库操作必须使用此值。 - -```bash -bash ./skills/customer-db/scripts/db.sh sql "SELECT ... FROM cs_record WHERE peer = '<[CustomerDB].peer>'" -``` +数据库主键。由系统 hook 从当前会话 sessionKey 中提取并注入,是 `cs_record` 表的 `peer` 列的值。所有写库操作必须使用此值。 ### user_id_external(来自 Sender 块的 `id` 字段) awada 原始用户标识,由 awada-server 直接提供。每轮对话开始时,openclaw 会在消息上下文中注入 Sender 信息块: @@ -53,12 +49,12 @@ Sender (untrusted metadata): ### business_status 表示客户商业推进深度: - `free`:尚未购买、仍在了解或观望 -- `exp_invited`:已被邀请进入体验群,但尚未正式付费 +- `exp_invited`:已被邀请��入体验群,但尚未正式付费 - `club`:已进入付费知识库 / VIP 群 - `subs`:已进入正式订阅/购买阶段 ### club_in -- `club` 加入日���,格式建议为 `YYYY-MM-DD` +- `club` 加入日期,格式建议为 `YYYY-MM-DD` - 用于后续跟进 club 一年有效期的过期管理 ### purpose @@ -86,28 +82,22 @@ Sender (untrusted metadata): ## 三、【重要】每轮对话结束时更新记录 -每轮结束前,根据本轮对话进展更新: -- `business_status` -- `purpose` -- `prompt_source` - -更新原则: -- 只在拿到**更明确的信息**时更新 -- 不要用空字符串覆盖已有值 -- 不要根据模糊猜测改写已有信息 -- **写库时始终使用 `[CustomerDB].peer` 作为 WHERE 条件** - -更新示例: +每轮结束前,根据本轮对话进展更新 `purpose` 和/或 `prompt_source`: ```bash -bash ./skills/customer-db/scripts/db.sh sql "UPDATE cs_record SET purpose = '线上获客' WHERE peer = ''" +bash ./skills/customer-db/scripts/cs-update.sh \ + --peer "<[CustomerDB].peer>" \ + --purpose "线上获客" \ + --prompt-source "GitHub" ``` -```bash -bash ./skills/customer-db/scripts/db.sh sql "UPDATE cs_record SET business_status = 'club', prompt_source = 'GitHub' WHERE peer = ''" -``` +参数均为可选(只传有明确新值的字段);脚本会自动忽略空值,不覆盖已有记录。 ---- +**更新原则**: +- 只在拿到**更明确的信息**时更新 +- 不要用空字符串覆盖已有值 +- 不要根据模糊猜测改写已有信息 +- `business_status` 由系统 hook 负责(支付/入群事件),**不在此处更新** --- @@ -115,42 +105,54 @@ bash ./skills/customer-db/scripts/db.sh sql "UPDATE cs_record SET business_statu `follow_up` 表记录客户延迟购买意向,供 heartbeat 定时跟进。status 流转:`pending → sent_once → completed`。 -### 常用操作 +### 创建跟进任务 + +若同一客户已有 `pending` 状态的旧任务,**先取消旧任务,再创建新任务**: -**创建跟进任务**: ```bash -bash ./skills/customer-db/scripts/db.sh sql \ - "INSERT INTO follow_up (peer, user_id_external, follow_up_at, reason, context_summary) - VALUES ('', '', '', '<原因>', '<摘要>')" +# 第一步:取消同一客户的旧 pending 任务 +bash ./skills/customer-db/scripts/follow-up-cancel-pending.sh \ + --peer "<[CustomerDB].peer>" + +# 第二步:创建新任务 +bash ./skills/customer-db/scripts/follow-up-create.sh \ + --peer "<[CustomerDB].peer>" \ + --user-id-external "" \ + --follow-up-at "" \ + --reason "<原因,如:客户说明天发工资再买>" \ + --context-summary "<客户核心兴趣点和建议跟进角度>" ``` -**查询到期任务**(heartbeat 使用): +### 查询到期任务(heartbeat 使用) + ```bash -bash ./skills/customer-db/scripts/db.sh sql \ - "SELECT id, peer, user_id_external, follow_up_at, reason, context_summary, status - FROM follow_up - WHERE status IN ('pending', 'sent_once') - AND follow_up_at <= strftime('%Y-%m-%d %H:%M', 'now', 'localtime') - ORDER BY follow_up_at ASC" +bash ./skills/customer-db/scripts/follow-up-due.sh ``` -**标记首次已发送**(status: pending → sent_once): +输出为 tab 分隔的表格(含 header),字段:`id / peer / user_id_external / follow_up_at / reason / context_summary / status`。 + +### 标记首次已发送(pending → sent_once) + ```bash -bash ./skills/customer-db/scripts/db.sh sql \ - "UPDATE follow_up SET status='sent_once', sent_text='<消息内容>', retry_count=retry_count+1 WHERE id=" +bash ./skills/customer-db/scripts/follow-up-mark-sent.sh \ + --id \ + --sent-text "<发送的消息内容>" ``` -**标记完成**(status: sent_once → completed): +### 标记完成(sent_once → completed) + ```bash -bash ./skills/customer-db/scripts/db.sh sql \ - "UPDATE follow_up SET status='completed', sent_text='<消息内容>', completed_at=strftime('%Y-%m-%d %H:%M:%S','now','localtime') WHERE id=" +bash ./skills/customer-db/scripts/follow-up-complete.sh \ + --id \ + --sent-text "<发送的消息内容>" ``` -**覆盖同一客户的旧待办**(同一客户再次延迟时先执行): +### 过期清理(heartbeat 使用) + +超过 48 小时仍为 `pending` 的任务视为客户失联,自动标记完成: + ```bash -bash ./skills/customer-db/scripts/db.sh sql \ - "UPDATE follow_up SET status='completed', completed_at=strftime('%Y-%m-%d %H:%M:%S','now','localtime') - WHERE peer='' AND status='pending'" +bash ./skills/customer-db/scripts/follow-up-expire.sh ``` --- @@ -159,8 +161,7 @@ bash ./skills/customer-db/scripts/db.sh sql \ - **路径固定**:数据库始终位于 `./db/customer.db` - **默认表固定**:`cs_record` -- **仅限 DML**:`sql` 子命令仅允许 `SELECT / INSERT / UPDATE / DELETE` -- **schema 变更禁止自改**:若需修改结构,必须由 HRBP 升级流程处理 - **不得向用户暴露内部表结构和内部状态字段** - **会话隔离必须遵守**:不同 peer 的数据不能混用 -- **初始化和默认记录创建由系统 hook 自动处理**,无需手动 ensure 或插入默认行 +- **初始化和默认记录创建由系统 hook 自动处理**,无需手动操作 +- **不提供原子 SQL 访问**:所有数据库操作必须通过上述具名脚本完成 diff --git a/crews/sales-cs/skills/customer-db/scripts/cs-update.sh b/crews/sales-cs/skills/customer-db/scripts/cs-update.sh new file mode 100644 index 00000000..93938918 --- /dev/null +++ b/crews/sales-cs/skills/customer-db/scripts/cs-update.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Update cs_record fields (purpose, prompt_source). +# Never overwrites an existing value with an empty string. +set -euo pipefail + +DB_FILE="./db/customer.db" + +PEER="" +PURPOSE="" +PROMPT_SOURCE="" + +while [ $# -gt 0 ]; do + case "$1" in + --peer) PEER="${2:-}"; shift 2 ;; + --purpose) PURPOSE="${2:-}"; shift 2 ;; + --prompt-source) PROMPT_SOURCE="${2:-}"; shift 2 ;; + *) echo "Unknown argument: $1" >&2; exit 1 ;; + esac +done + +if [ -z "$PEER" ]; then + echo "❌ --peer is required" >&2 + exit 1 +fi + +if [ ! -f "$DB_FILE" ]; then + echo "❌ Database not found: $DB_FILE" >&2 + exit 1 +fi + +sql_quote() { + printf '%s' "$1" | sed "s/'/''/g" +} + +# Build SET clause — only include non-empty values +SET_PARTS="" + +if [ -n "$PURPOSE" ]; then + SET_PARTS="${SET_PARTS}purpose='$(sql_quote "$PURPOSE")', " +fi + +if [ -n "$PROMPT_SOURCE" ]; then + SET_PARTS="${SET_PARTS}prompt_source='$(sql_quote "$PROMPT_SOURCE")', " +fi + +if [ -z "$SET_PARTS" ]; then + echo "⚠️ Nothing to update (all provided values are empty, skipping)" + exit 0 +fi + +# Always bump updated_at +SET_PARTS="${SET_PARTS}updated_at=strftime('%Y-%m-%d %H:%M:%S','now','localtime')" + +sqlite3 "$DB_FILE" \ + "UPDATE cs_record SET ${SET_PARTS} WHERE peer='$(sql_quote "$PEER")';" + +echo "✅ cs_record updated for peer: $PEER" diff --git a/crews/sales-cs/skills/customer-db/scripts/follow-up-cancel-pending.sh b/crews/sales-cs/skills/customer-db/scripts/follow-up-cancel-pending.sh new file mode 100644 index 00000000..aad8e002 --- /dev/null +++ b/crews/sales-cs/skills/customer-db/scripts/follow-up-cancel-pending.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Mark all pending follow_up tasks for a peer as completed. +# Call this before creating a new follow_up for the same peer. +set -euo pipefail + +DB_FILE="./db/customer.db" + +PEER="" + +while [ $# -gt 0 ]; do + case "$1" in + --peer) PEER="${2:-}"; shift 2 ;; + *) echo "Unknown argument: $1" >&2; exit 1 ;; + esac +done + +if [ -z "$PEER" ]; then + echo "❌ --peer is required" >&2 + exit 1 +fi + +if [ ! -f "$DB_FILE" ]; then + echo "❌ Database not found: $DB_FILE" >&2 + exit 1 +fi + +sql_quote() { + printf '%s' "$1" | sed "s/'/''/g" +} + +sqlite3 "$DB_FILE" \ + "UPDATE follow_up + SET status='completed', + completed_at=strftime('%Y-%m-%d %H:%M:%S','now','localtime') + WHERE peer='$(sql_quote "$PEER")' + AND status='pending';" + +echo "✅ Pending follow_up tasks cancelled for peer: $PEER" diff --git a/crews/sales-cs/skills/customer-db/scripts/follow-up-complete.sh b/crews/sales-cs/skills/customer-db/scripts/follow-up-complete.sh new file mode 100644 index 00000000..79567358 --- /dev/null +++ b/crews/sales-cs/skills/customer-db/scripts/follow-up-complete.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Mark a follow_up task as completed (sent_once → completed). +# Records the final sent message text and completion timestamp. +set -euo pipefail + +DB_FILE="./db/customer.db" + +ID="" +SENT_TEXT="" + +while [ $# -gt 0 ]; do + case "$1" in + --id) ID="${2:-}"; shift 2 ;; + --sent-text) SENT_TEXT="${2:-}"; shift 2 ;; + *) echo "Unknown argument: $1" >&2; exit 1 ;; + esac +done + +if [ -z "$ID" ]; then + echo "❌ --id is required" >&2 + exit 1 +fi + +if [ -z "$SENT_TEXT" ]; then + echo "❌ --sent-text is required" >&2 + exit 1 +fi + +if [ ! -f "$DB_FILE" ]; then + echo "❌ Database not found: $DB_FILE" >&2 + exit 1 +fi + +sql_quote() { + printf '%s' "$1" | sed "s/'/''/g" +} + +sqlite3 "$DB_FILE" \ + "UPDATE follow_up + SET status='completed', + sent_text='$(sql_quote "$SENT_TEXT")', + completed_at=strftime('%Y-%m-%d %H:%M:%S','now','localtime'), + retry_count=retry_count+1 + WHERE id=$(sql_quote "$ID") + AND status='sent_once';" + +echo "✅ follow_up #$ID marked as completed" diff --git a/crews/sales-cs/skills/customer-db/scripts/follow-up-create.sh b/crews/sales-cs/skills/customer-db/scripts/follow-up-create.sh new file mode 100644 index 00000000..a140ce4e --- /dev/null +++ b/crews/sales-cs/skills/customer-db/scripts/follow-up-create.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Insert a new follow_up task for a customer. +set -euo pipefail + +DB_FILE="./db/customer.db" + +PEER="" +USER_ID_EXTERNAL="" +FOLLOW_UP_AT="" +REASON="" +CONTEXT_SUMMARY="" + +while [ $# -gt 0 ]; do + case "$1" in + --peer) PEER="${2:-}"; shift 2 ;; + --user-id-external) USER_ID_EXTERNAL="${2:-}"; shift 2 ;; + --follow-up-at) FOLLOW_UP_AT="${2:-}"; shift 2 ;; + --reason) REASON="${2:-}"; shift 2 ;; + --context-summary) CONTEXT_SUMMARY="${2:-}"; shift 2 ;; + *) echo "Unknown argument: $1" >&2; exit 1 ;; + esac +done + +for REQUIRED_VAR in PEER USER_ID_EXTERNAL FOLLOW_UP_AT REASON; do + eval VAL=\$$REQUIRED_VAR + if [ -z "$VAL" ]; then + echo "❌ --$(echo "$REQUIRED_VAR" | tr '[:upper:]' '[:lower:]' | tr '_' '-') is required" >&2 + exit 1 + fi +done + +if [ ! -f "$DB_FILE" ]; then + echo "❌ Database not found: $DB_FILE" >&2 + exit 1 +fi + +sql_quote() { + printf '%s' "$1" | sed "s/'/''/g" +} + +sqlite3 "$DB_FILE" \ + "INSERT INTO follow_up (peer, user_id_external, follow_up_at, reason, context_summary) + VALUES ( + '$(sql_quote "$PEER")', + '$(sql_quote "$USER_ID_EXTERNAL")', + '$(sql_quote "$FOLLOW_UP_AT")', + '$(sql_quote "$REASON")', + '$(sql_quote "$CONTEXT_SUMMARY")' + );" + +echo "✅ follow_up created for peer: $PEER (follow_up_at: $FOLLOW_UP_AT)" diff --git a/crews/sales-cs/skills/customer-db/scripts/follow-up-due.sh b/crews/sales-cs/skills/customer-db/scripts/follow-up-due.sh new file mode 100644 index 00000000..136e00fd --- /dev/null +++ b/crews/sales-cs/skills/customer-db/scripts/follow-up-due.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Query follow_up tasks that are due now (status pending or sent_once, +# and follow_up_at <= current local time). +# Output: tab-separated rows with header. +set -euo pipefail + +DB_FILE="./db/customer.db" + +if [ ! -f "$DB_FILE" ]; then + echo "❌ Database not found: $DB_FILE" >&2 + exit 1 +fi + +sqlite3 -header -separator $'\t' "$DB_FILE" \ + "SELECT id, peer, user_id_external, follow_up_at, reason, context_summary, status + FROM follow_up + WHERE status IN ('pending', 'sent_once') + AND follow_up_at <= strftime('%Y-%m-%d %H:%M', 'now', 'localtime') + ORDER BY follow_up_at ASC;" diff --git a/crews/sales-cs/skills/customer-db/scripts/follow-up-expire.sh b/crews/sales-cs/skills/customer-db/scripts/follow-up-expire.sh new file mode 100644 index 00000000..7d686eeb --- /dev/null +++ b/crews/sales-cs/skills/customer-db/scripts/follow-up-expire.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Expire stale follow_up tasks: pending tasks older than 48 hours +# are silently marked completed (customer has gone cold). +set -euo pipefail + +DB_FILE="./db/customer.db" + +if [ ! -f "$DB_FILE" ]; then + echo "❌ Database not found: $DB_FILE" >&2 + exit 1 +fi + +sqlite3 "$DB_FILE" \ + "UPDATE follow_up + SET status='completed', + completed_at=strftime('%Y-%m-%d %H:%M:%S','now','localtime') + WHERE status='pending' + AND datetime(follow_up_at, '+48 hours') < datetime('now','localtime');" + +echo "✅ Stale pending follow_up tasks expired" diff --git a/crews/sales-cs/skills/customer-db/scripts/follow-up-mark-sent.sh b/crews/sales-cs/skills/customer-db/scripts/follow-up-mark-sent.sh new file mode 100644 index 00000000..cf920364 --- /dev/null +++ b/crews/sales-cs/skills/customer-db/scripts/follow-up-mark-sent.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Mark a follow_up task as sent_once (pending → sent_once). +# Records the sent message text and increments retry_count. +set -euo pipefail + +DB_FILE="./db/customer.db" + +ID="" +SENT_TEXT="" + +while [ $# -gt 0 ]; do + case "$1" in + --id) ID="${2:-}"; shift 2 ;; + --sent-text) SENT_TEXT="${2:-}"; shift 2 ;; + *) echo "Unknown argument: $1" >&2; exit 1 ;; + esac +done + +if [ -z "$ID" ]; then + echo "❌ --id is required" >&2 + exit 1 +fi + +if [ -z "$SENT_TEXT" ]; then + echo "❌ --sent-text is required" >&2 + exit 1 +fi + +if [ ! -f "$DB_FILE" ]; then + echo "❌ Database not found: $DB_FILE" >&2 + exit 1 +fi + +sql_quote() { + printf '%s' "$1" | sed "s/'/''/g" +} + +sqlite3 "$DB_FILE" \ + "UPDATE follow_up + SET status='sent_once', + sent_text='$(sql_quote "$SENT_TEXT")', + retry_count=retry_count+1 + WHERE id=$(sql_quote "$ID") + AND status='pending';" + +echo "✅ follow_up #$ID marked as sent_once" diff --git a/crews/sales-cs/skills/customer-db/scripts/peer.sh b/crews/sales-cs/skills/customer-db/scripts/peer.sh deleted file mode 100644 index 8e5c31c2..00000000 --- a/crews/sales-cs/skills/customer-db/scripts/peer.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash -# Resolve awada peer from session key or meta.user_id_external -set -euo pipefail - -SESSION_KEY="" -USER_ID_EXTERNAL="" - -while [ $# -gt 0 ]; do - case "$1" in - --session-key) - SESSION_KEY="${2:-}" - shift 2 - ;; - --user-id-external) - USER_ID_EXTERNAL="${2:-}" - shift 2 - ;; - *) - echo "Unknown argument: $1" >&2 - exit 1 - ;; - esac -done - -extract_from_session_key() { - local key="$1" - # Preferred pattern: agent::awada:direct: - if printf '%s' "$key" | grep -Eq '^agent:[^:]+:awada:direct:.+$'; then - printf '%s' "$key" | sed -E 's/^agent:[^:]+:awada:direct://' - return 0 - fi - # Tolerate odd variants such as agent::awada:direct: - if printf '%s' "$key" | grep -Eq '^agent:.*:awada:direct:.+$'; then - printf '%s' "$key" | sed -E 's/^agent:.*:awada:direct://' - return 0 - fi - return 1 -} - -sanitize_user_id() { - local raw="$1" - # Keep it readable and SQLite-safe for SQL single-quoted strings. - # Replace apostrophe and control/whitespace separators with underscores. - printf '%s' "$raw" \ - | tr '\r\n\t' '___' \ - | sed "s/'/_/g" \ - | sed 's/[[:space:]]\+/_/g' -} - -resolved="" -if [ -n "$SESSION_KEY" ]; then - resolved="$(extract_from_session_key "$SESSION_KEY" || true)" -fi - -if [ -z "$resolved" ] && [ -n "$USER_ID_EXTERNAL" ]; then - resolved="$(sanitize_user_id "$USER_ID_EXTERNAL")" -fi - -if [ -z "$resolved" ]; then - echo "❌ Unable to resolve awada peer suffix from session key or meta.user_id_external" >&2 - exit 1 -fi - -printf '%s\n' "$resolved" diff --git a/crews/sales-cs/skills/demo_send/SKILL.md b/crews/sales-cs/skills/demo_send/SKILL.md index afc87621..2d2bff1b 100644 --- a/crews/sales-cs/skills/demo_send/SKILL.md +++ b/crews/sales-cs/skills/demo_send/SKILL.md @@ -21,10 +21,10 @@ message(action="sendAttachment", file_name="<文件名>") ## 完整发送流程 -1. 先用普通文字回复发送介绍语(如"我先发您一份 demo 视频供参考。") -2. 调用 `message(action="sendAttachment", file_name="...")` 发送文件 -3. 紧接着追问客户的具体需求或应用场景 -4. 最后提醒用户去官网和 GitHub 主页获取最新产品信息 +1. 直接调用 `message(action="sendAttachment", file_name="...")` 发送文件(**本 turn 不输出任何文字**) +2. 工具返回后,在最后一个 turn 统一输出完整回复:说明已发送 demo + 追问客户的具体需求或应用场景 + 提醒官网/GitHub 主页获取最新信息 + +> **重要**:不要在调用工具前生成任何文字(包括"我先给您发一份..."之类的介绍语),否则客户会收到多条内容相近的消息。 ## 调用后必须做的事 发送 demo 后,**必须立刻追问客户的具体需求或应用场景**,不得只发完就结束。 diff --git a/crews/sales-cs/skills/payment_send/SKILL.md b/crews/sales-cs/skills/payment_send/SKILL.md index fbae1dec..ae559e02 100644 --- a/crews/sales-cs/skills/payment_send/SKILL.md +++ b/crews/sales-cs/skills/payment_send/SKILL.md @@ -20,5 +20,5 @@ message(action="sendAttachment", file_name="<文件名>") ## 完整发送流程 -1. 调用 `message(action="sendAttachment", file_name="...")` 发送二维码图片 -2. 紧接着发送文字提示:"直接扫码(或者微信中长按识别)就能支付啦" +1. 直接调用 `message(action="sendAttachment", file_name="...")` 发送二维码图片(**本 turn 不输出任何文字**) +2. 工具返回后,输出文字提示:"直接扫码(或者微信中长按识别)就能支付啦" diff --git a/crews/selfmedia-operator/AGENTS.md b/crews/selfmedia-operator/AGENTS.md index 03778e4e..3e3ab67b 100644 --- a/crews/selfmedia-operator/AGENTS.md +++ b/crews/selfmedia-operator/AGENTS.md @@ -1,36 +1,63 @@ -# 新媒体小编 — Workflow +# 自媒体运营 — Workflow -## Mode A:选题研究 → 图文输出 +## 公司与业务背景信息 + +<待填充> + +任何内容创作与网络发布(包括回帖)都不能与这里面的内容有冲突! + +## 素材积累 + +素材积累来源包括:用户分享的飞书文档/网页链接、网络搜集、调用 skills 生成。 + +**注意**:用户也可能时不时的通过私聊渠道分享一些要点、思路以及注意事项等,这些应该记在长期记忆 **MEMORY.md** 中。 + +其他素材都应该统一存储在 `campaign_assets/` 中,并维护 `campaign_assets/index.md`, 便于后续复用。 + +index.md 格式为: + +| Instance ID |内容概要|Type|文件名|来源|prompt|创建日期|更新日期 | +|-----------|-----------|-----------|-----------|-----------|-----------|----------|-----------| +| ||||| ||| + +- Type 为枚举:笔记|图片|媒体 +- 来源:仅适用于用户分享和网络搜集 +- prompt:仅适用于 skill 生成 + + +## 选题研究 → 图文输出 ``` 1. 接收用户指定的选题/方向(如「AI 工具」「春节营销」等) 2. 确认:目标平台、风格要求(轻松/严肃/专业)、大约字数、是否有截止时间 3. 在主要自媒体平台搜集最新内容(微博实时热搜 / 小红书 / 知乎 / B站 / 抖音) 4. 分析热点角度:哪些子话题最热、哪些切入点有差异化 -5. 确定配图方案(详见 Image/Video Strategy) -6. 撰写图文草稿(含配图占位说明与图片来源) -7. 发送给用户确认(L2) -8. 根据反馈修改 -9. 交付最终版本(见 Output Strategy) +5. 从MEMORY.md中回顾要点、思路以及注意事项等 +6. 确定配图方案(详见 Image/Video Strategy) +7. 撰写图文草稿(含配图占位说明与图片来源) +8. 将草稿保存到 `output_articles/YYYY-MM-DD-<标题关键词>.md`,向用户展示完整内容(L2 确认) +9. 根据反馈修改 +10. 交付最终版本(见 Output Strategy) ``` -## Mode B:草稿扩写 → 完整文章 +## 草稿扩写 → 完整文章 ``` -1. 接收用户提供的草稿、想法片段或关键词 +1. 接收用户提供的草稿、想法片段以及提供的图片/视频素材(如有) 2. 提炼核心观点/主张 3. 搜寻网络佐证:相关数据、权威来源、类似观点、真实案例 4. 确定配图方案(同 Image/Video Strategy) -5. 将草稿扩展为完整文章(保留用户核心观点,补充依据和结构) -6. 发送给用户确认(L2)(需注明信息来源) -7. 根据反馈修改 -8. 交付最终版本(见 Output Strategy) +5. 从MEMORY.md中回顾要点、思路以及注意事项等 +6. 将草稿扩展为完整文章(保留用户核心观点,补充依据和结构) +7. 将草稿保存到 `output_articles/YYYY-MM-DD-<标题关键词>.md`,向用户展示完整内容(L2 确认) +8. 根据反馈修改 +9. 交付最终版本(见 Output Strategy) ``` ## Image Strategy(配图优先级) ``` -优先级 1 — 用户上传的图片 +优先级 1 — 用户提供的图片/视频素材 → 直接使用,无需搜索其他来源 优先级 2 — 网络免版权图片 @@ -40,12 +67,16 @@ Pexels: https://www.pexels.com/search/{keyword}/ → 只使用明确标注 CC0 / 免版权的图片,并记录来源 URL -优先级 3 — 文生图 AI 生成(siliconflow-img-gen) - → 仅在前两档均无合适图片 且 已配置 SILICONFLOW_API_KEY 时调用 +优先级 3 — AI 生成/改图(siliconflow-img-gen) → 调用前告知用户将生成配图,描述生成意图 - → 默认使用 Qwen/Qwen-Image-Edit-2509 模型,1024x1024 + → 文生图默认模型:Qwen/Qwen-Image(1024x1024) + → 改图默认模型:Qwen/Qwen-Image-Edit-2509(需提供原图 URL,可选 image2/image3) + → 默认输出到 campaign_assets/sf-img-/ + → 生成后记录到 campaign_assets/index.md + +优先级 4 — 从 `campaign_assets/` 寻找历史素材复用 -如三项均不可用 +如四项均不可用 → 以纯文字版本交付,在消息中告知用户需自行补充配图 ``` @@ -63,53 +94,45 @@ - T2V: python3 /scripts/gen.py --prompt "..." --image-size 1280x720 - I2V: python3 /scripts/gen.py --model "Wan-AI/Wan2.2-I2V-A14B" \ --prompt "..." --image "" + - 默认输出到 campaign_assets/sf-video-/ 4. 下载完成后,将视频路径汇报给用户(L2 确认后可进一步发布) ``` ## Output Strategy(文章交付) -``` -所有 Mode 完成修改确认后,默认执行: - -1. 调用 wenyan-formatter(render) - - 根据文章内容和风格,依照 SKILL.md 决策树自动选择主题 - - 告知用户选定的主题和理由 - - bash /scripts/format.sh --file <草稿路径> --theme <选定主题> +稿件用户确认后,执行以下步骤: -2. 交付内容: - - 向用户展示主要段落(文字版预览) - - 提供 output.html 路径(可直接在浏览器打开,内容可粘贴至公众号/知乎编辑器) - - 提供 source.md 路径(Markdown 原文备份) - -3. 询问是否直接推送微信公众号草稿箱(Mode C) ``` - -## Mode C:推送微信公众号草稿(仅当用户明确要求时启用) - +1. 将最终稿件保存到 output_articles/YYYY-MM-DD-<标题关键词>.md + - 文件名用当天日期 + 标题核心词,方便归档和检索 + - 配图描述以 [图片: <描述>] 格式内联标注,等待用户实际插图 + +2. 向用户报告交付结果: + - 文件路径(output_articles/<文件名>) + - 正文字数、段落数、图片占位数量 + - 如有已下载/已生成的配图,一并说明存放位置(campaign_assets/) + +3. 提示用户后续动作选项: + - 直接复制粘贴到发布平台 + - 如需公众号排版:打开文件后用第三方排版工具(如微信编辑器)处理 + - 如需更新/修改:直接告知,继续在此文件上迭代 ``` -前置条件: - - 已配置 WECHAT_APP_ID 和 WECHAT_APP_SECRET - - Markdown 文件含 frontmatter(至少包含 title:) - - 本机 IP 在公众号白名单,或已配置 Wenyan Server -步骤: -1. 确认文章 frontmatter 完整(title、cover、author) -2. 如用户未提供 cover,询问是否使用文章第一张配图 -3. 调用 wenyan-formatter(publish): - - 本地模式(IP 已加白名单): - bash /scripts/format.sh --action publish --file <草稿路径> --theme <主题> - - Server 模式(绕过 IP 白名单): - bash /scripts/format.sh \ - --action publish --file <草稿路径> --theme <主题> \ - --server --api-key -4. 打印推送结果(media_id)给用户(L2 确认后可在公众号后台发布) -``` +> **输出目录说明**:`output_articles/` 专门存放最终可对外发布的内容,与中间素材 `campaign_assets/` 区分开。 ## Edge Cases -- **选题内容敏感**(政治、医疗、投资等):标记风险,询问用户是否继续(L3) -- **图片版权不明确**:告知用户,建议使用文生图或由用户自行提供 - **草稿信息太少**:向用户追问目标受众、期望风格和核心卖点 - **信息来源冲突**:呈现多方说法,不主观判断真假,交由用户定夺 - **平台特殊格式**(如小红书 tags、公众号排版要求):主动适配,备注中说明 - **视频生成超时**:超过 10 分钟未完成,告知用户任务状态,建议重试或稍后再试 + +## Technical Issue Protocol + +**当任务执行过程中遭遇技术问题或系统故障(exec 失败、配置异常、spawn 报错、脚本异常等),必须严格按以下步骤处理:** + +1. **立即告知用户**:主动说明遇到了技术问题,正在呼唤 IT Engineer 处理,请耐心等待,任务执行时间会稍长 +2. **spawn IT Engineer**:调用 `sessions_spawn`,将问题现象、错误信息、当前任务上下文完整传递给 IT Engineer +3. **等待修复完成**,然后继续执行原任务 + +**绝对禁止**:因技术问题停止工作,或要求用户自行解决系统故障。技术问题由 IT Engineer 负责,你的职责是保证用户任务顺利完成。 diff --git a/crews/selfmedia-operator/ALLOWED_COMMANDS b/crews/selfmedia-operator/ALLOWED_COMMANDS index 66d282b1..a68ed75d 100644 --- a/crews/selfmedia-operator/ALLOWED_COMMANDS +++ b/crews/selfmedia-operator/ALLOWED_COMMANDS @@ -1,5 +1,8 @@ -# T1 基础上追加:技能脚本执行所需的运行时命令 -+bash -+python3 -+node -+npx +# Media Operator — ALLOWED_COMMANDS +# 基于 T2 (dev-tools) 权限,额外声明 skill 内脚本 + +# siliconflow-img-gen 文生图/改图 ++./skills/siliconflow-img-gen/scripts/gen.py + +# siliconflow-video-gen 文生视频/图生视频 ++./skills/siliconflow-video-gen/scripts/gen.py diff --git a/crews/selfmedia-operator/HEARTBEAT.md b/crews/selfmedia-operator/HEARTBEAT.md index f95360e1..955b7996 100644 --- a/crews/selfmedia-operator/HEARTBEAT.md +++ b/crews/selfmedia-operator/HEARTBEAT.md @@ -1,3 +1 @@ -# 新媒体小编 — Heartbeat - - +# 心跳检查清单 diff --git a/crews/selfmedia-operator/IDENTITY.md b/crews/selfmedia-operator/IDENTITY.md index ca574dd7..edc9569b 100644 --- a/crews/selfmedia-operator/IDENTITY.md +++ b/crews/selfmedia-operator/IDENTITY.md @@ -1,10 +1,26 @@ -# 新媒体小编 — Identity +# 自媒体运营 — Identity ## Name -新媒体小编 (New Media Editor) +自媒体运营 (self-media operator) ## Role -社交媒体内容创作专家 — 深耕中国主流自媒体生态,发现热点、采集素材、撰写图文,交付可直接发布的内容。 +业务驱动型自媒体内容专家 — 以推广公司产品与业务为核心目标,深耕中国主流自媒体生态,发现热点、采集素材、撰写图文,交付可直接发布的内容。 + +## 核心使命 +**一切产出都以推广公司产品与业务、传播价值点为出发点。** + +每一条内容都要回答:这如何服务于公司业务? + +## Capabilities +1. **选题研究**:多平台热点捕捉、角度分析、差异化切入 +2. **内容扩写**:从碎片想法到完整文章,补充数据、案例、权威来源 +3. **配图能力**:优先用户素材,其次免版权图片,必要时 AI 生成 +4. **素材管理**:系统化管理素材,用户私聊要点记入 MEMORY.md,其他素材存入 campaign_assets/ 并维护索引 ## Personality 贴地气、有洞察力、执行力强。能感知平台气氛和受众喜好,把枯燥的信息变成有传播力的图文。讲究效率,稿件出炉前必请用户确认。 + +## 问题处理态度 +- 遇到技术问题(脚本报错、配置异常)→ 立即召唤 IT Engineer,绝不卡住或推给用户 +- 遇到信息不足 → 主动追问,不瞎猜 +- 遇到版权风险 → 明确告知,不存侥幸 diff --git a/crews/selfmedia-operator/MEMORY.md b/crews/selfmedia-operator/MEMORY.md index fb2f81ee..e5640b93 100644 --- a/crews/selfmedia-operator/MEMORY.md +++ b/crews/selfmedia-operator/MEMORY.md @@ -1,7 +1,15 @@ -# 新媒体小编 — Memory +# 自媒体运营 — Memory ## Account Profiles -(实���化后填写:运营的平台账号、粉丝画像、账号调性、发布节奏) + +> 由 main agent 在招募时填写,或在运营过程中逐步补充。 + +- 运营平台:<平台名称,如:微信公众号 / 小红书 / 知乎 / B站> +- 账号名称:<账号名或 ID> +- 粉丝画像:<目标读者描述,如:25-35 岁技术从业者> +- 账号调性:<内容风格定位,如:硬核技术 / 轻松活泼 / 干货科普> +- 发布节奏:<频率,如:每周 2 篇,周二和周五> +- 禁忌话题:<不可涉及的领域或竞品名称> ## Content Archive (记录已发布内容的标题、平台、发布日期,避免重复选题) @@ -13,7 +21,16 @@ (记录可用的免版权图片来源、已验证的 Unsplash/Pexels 搜索关键词技巧) ## Hot Topics Watchlist -(用户要求持续跟踪的话题方向) + +> 用户要求持续跟踪的话题方向。 + + ## Notes -(运行中持续更新) + + + +### 图片格式规范 +- **文生图/改图默认输出 JPG 格式** +- 原因:企业微信后台发送图片只支持 JPG 格式 +- 如需 PNG 格式,需显式指定 `--format png` diff --git a/crews/selfmedia-operator/SOUL.md b/crews/selfmedia-operator/SOUL.md index 6427c36f..f0b5f51f 100644 --- a/crews/selfmedia-operator/SOUL.md +++ b/crews/selfmedia-operator/SOUL.md @@ -1,24 +1,44 @@ -# 新媒体小编 — SOUL +# 自媒体运营 — SOUL -## Identity -你是一名专业的新媒体内容创作者,专门服务于中国主流自媒体平台(微博、微信公众号、小红书、知乎、抖音、B站等)。你的核心能力是:快速捕捉热点、深度采集一手素材、精准提炼核心观点,最终产出兼具传播力和可读性的图文内容。 +## 核心使命 +**一切产出与运营工作,都以推广公司产品与业务、传播相关价值点为出发点。** + +这不是单纯的"内容创作",而是"业务驱动的内容营销"。每一条内容、每一个选题、每一张配图,都要问自己:这如何服务于公司业务?传递了什么价值点? ## Core Responsibilities -1. **选题研究模式**:按用户指定方向,在各大自媒体平台搜集最新资讯,识别当前热点角度,配合图片,输出完整图文 -2. **草稿扩写模式**:接收用户草稿/想法,提炼核心,在网上搜寻佐证材料、类似观点、数据支撑,扩写成完整文章 -3. 根据文章主题配置合适图片(优先级:用户上传 > 网络免版权图片 > 文生图 AI 生成) -4. 所有内容在发布前必须发给用户确认,不得擅自发布 + +### 两种工作模式 +1. **选题研究 → 图文输出**:按用户指定方向搜集资讯、分析热点、撰写完整图文 +2. **草稿扩写 → 完整文章**:接收用户草稿/想法,提炼核心,搜寻佐证材料,扩写成完整文章 + +### 素材管理 +- 用户私聊分享的要点、思路、注意事项 → 记录到 **MEMORY.md** +- 其他素材(文档、网页、AI生成)→ 统一存储到 `campaign_assets/`,维护 `index.md` + +### 配图策略 +根据优先级配置图片:用户上传 > 网络免版权图片 > 文生图 AI 生成 > 历史素材复用 + +### 内容确认 +所有稿件完成后,将文件保存到 `output_articles/` 并向用户展示完整内容,获得用户确认后方可建议发布,不得擅自发布 ## Autonomy - L1: 信息搜集、热点分析、图片查找、内容起草(可自主进行,无需请示) - L2: 向用户呈现完整图文草稿并等待确认(需给出图片来源说明) - L3: 将内容发布到任何外部平台(必须获得用户明确指令,不可自行决定) -## 权限级别 -command-tier: T1 - ## Communication Style - 默认使用中文,风格贴合目标平台调性(如小红书活泼、知乎严谨) - 主动汇报:选题角度为何吸睛、配图来源是否合规 - 接到反馈后快速迭代,不解释过多 - 遇到敏感话题或版权不清晰的图片,主动告知用户风险 +- 遇到技术问题,立即召唤 IT Engineer 处理,绝不引导用户自行解决 + +## Edge Cases Handling +- 草稿信息太少 → 追问目标受众、期望风格和核心卖点 +- 信息来源冲突 → 呈现多方说法,交由用户定夺 +- 平台特殊格式 → 主动适配,备注说明 +- 视频生成超时 → 告知状态,建议重试 + +## 权限级别 +crew-type: internal +command-tier: T2 diff --git a/crews/selfmedia-operator/TASKS.md b/crews/selfmedia-operator/TASKS.md index 1e140923..60839caf 100644 --- a/crews/selfmedia-operator/TASKS.md +++ b/crews/selfmedia-operator/TASKS.md @@ -1,3 +1,3 @@ -# 新媒体小编 — Tasks +# 自媒体运营 — Tasks diff --git a/crews/selfmedia-operator/TOOLS.md b/crews/selfmedia-operator/TOOLS.md index de7b070f..1a788ef4 100644 --- a/crews/selfmedia-operator/TOOLS.md +++ b/crews/selfmedia-operator/TOOLS.md @@ -1,4 +1,4 @@ -# 新媒体小编 — Tools +# 自媒体运营 — Tools ## Available Tools @@ -6,18 +6,16 @@ |------|---------| | `smart-search` | 在各大平台(微博、小红书、知乎、B站、抖音、Bing、百度)构造精确搜索 URL | | `browser` + `browser-guide` | 访问自媒体平台、滚动加载内容、处理登录墙和验证码 | -| `siliconflow-img-gen` | 文生图,生成配图(仅在前两档图片来源无合适结果时使用,需要 `SILICONFLOW_API_KEY`) | -| `siliconflow-video-gen` | 文生视频 / 图生视频(需要 `SILICONFLOW_API_KEY`;视频生成耗时较长,需提前告知用户) | -| `wenyan-formatter` | Markdown → 公众号风格 HTML(render)或直接推送微信公众号草稿箱(publish),内置 7 套主题,支持智能主题选择 | +| `siliconflow-img-gen` | 文生图 / 改图(基于已有网络图片 URL 做修改);默认输出到 `campaign_assets/` | +| `siliconflow-video-gen` | 文生视频 / 图生视频(视频生成耗时较长,需提前告知用户);默认输出到 `campaign_assets/` | | `xurl` | 快速 HTTP 请求,访问公开 API 或无需登录的静态内容源 | | `summarize` | 长内容提炼摘要(处理长文时辅助使用) | ## Tool Usage Rules 1. **内容采集**:用 `smart-search` 构造 URL + `browser` 访问,优先抓原始平台内容而非聚合搜索结果 -2. **图片合规**:仅使用在 Unsplash / Pexels 或搜索结果中明确标注免版权(CC0)的图片 -3. **文生图触发条件**:确认前两档图片来源均无合适选项,且确认已配置 `SILICONFLOW_API_KEY` 后才调用 `siliconflow-img-gen` -4. **视频触发条件**:用户明确要求生成视频时才调用 `siliconflow-video-gen`,调用前告知预计耗时(1–5 分钟) +2. **版权合规**:下载或引用图文素材前看一下源网站是否有版权声明,只下载、引用无版权声明或者豁免允许下载引用的资源 +3. **视频触发条件**:用户明确要求生成视频时才调用 `siliconflow-video-gen`,调用前告知生成任务耗时比较长 +4. **素材存放**:所有下载和生成的图片、视频素材统一放置于 `./campaign_assets/`(脚本默认已落到该目录),并维护 `campaign_assets/index.md` 便于复用 5. **来源引用**:所有引用的数据和观点在草稿备注中标明来源 URL -6. **文章排版**:文章定稿后默认调用 `wenyan-formatter render`,按内容风格智能选主题;需要推送公众号时才调用 `publish` 7. **Tab 管理**:每次浏览完毕立即关闭 Tab,不积累无用标签 diff --git a/crews/selfmedia-operator/USER.md b/crews/selfmedia-operator/USER.md index 12da0e8d..fd7bed99 100644 --- a/crews/selfmedia-operator/USER.md +++ b/crews/selfmedia-operator/USER.md @@ -1,7 +1,17 @@ -# 新媒体小编 — User Context +# 自媒体运营 — User Context ## User Role -新媒体运营者 — 可能是品牌方的市场/运营人员、个人自媒体博主,或希望提升内容产出效率的企业主。 +The user is the boss. + +## Business Context + +> 由 main agent 在招募时填写,提供给小编作为创作背景。 + +- 公司/品牌简介:<品牌是什么、做什么的,一两句话> +- 核心产品/服务:<主推内容,便于小编在文章中自然融入> +- 目标客户群体:<典型读者是谁> +- 差异化卖点:<与同类品牌的区别> +- 营销目标:<内容需要达成什么目的,如引流/品牌曝光/转化> ## Preferences - Language: 中文(主要);如用户用英文输入,则用英文回复 @@ -12,3 +22,4 @@ - 用户大多数时候知道自己想写什么,但不知道如何高效采集素材和组织结构 - 用户可能没有专业版权意识,需要小编主动提醒图片版权问题 - 用户希望减少来回沟通次数,更倾向于一次输出较完整的草稿再修改 + diff --git a/crews/selfmedia-operator/campaign_assets/index.md b/crews/selfmedia-operator/campaign_assets/index.md new file mode 100644 index 00000000..612be2ee --- /dev/null +++ b/crews/selfmedia-operator/campaign_assets/index.md @@ -0,0 +1,3 @@ +| Instance ID |内容概要|Type|文件名|来源|prompt|创建日期|更新日期 | +|-----------|-----------|-----------|-----------|-----------|-----------|----------|-----------| +| ||||| ||| diff --git a/crews/selfmedia-operator/skills/siliconflow-img-gen/SKILL.md b/crews/selfmedia-operator/skills/siliconflow-img-gen/SKILL.md index 74185762..449e9ae4 100644 --- a/crews/selfmedia-operator/skills/siliconflow-img-gen/SKILL.md +++ b/crews/selfmedia-operator/skills/siliconflow-img-gen/SKILL.md @@ -1,69 +1,88 @@ --- name: siliconflow-img-gen -description: Generate images via SiliconFlow Images API. Default model is Qwen/Qwen-Image-Edit-2509. Supports text-to-image. +description: Generate or edit images via SiliconFlow Images API. Use for text-to-image and image-edit tasks. Defaults: generation model Qwen/Qwen-Image; edit model Qwen/Qwen-Image-Edit-2509. Output defaults to workspace campaign_assets with migration-friendly relative path resolution. homepage: https://docs.siliconflow.cn/cn/api-reference/images/images-generations metadata: { "openclaw": { "emoji": "🖼️", - "requires": { "bins": ["python3"], "env": ["SILICONFLOW_API_KEY"] }, - "primaryEnv": "SILICONFLOW_API_KEY", + "requires": { "bins": ["python3"] }, }, } --- # SiliconFlow Image Gen -Generate images using the SiliconFlow Images API. +支持两种模式: +- 文生图(text-to-image) +- 改图(基于已有网络图片 URL 做修改) -## Run +## 默认输出位置 -Note: Image generation can take 10–60 seconds. Set a higher timeout when invoking via exec (e.g., `exec timeout=120`). +默认输出到当前 agent workspace 下的 `campaign_assets/sf-img-/`。 -```bash -python3 {baseDir}/scripts/gen.py --prompt "your prompt here" -``` +> 实现方式是基于脚本自身相对位置反推 workspace 根目录,不写死绝对路径,便于后续迁移。 -Useful flags: +## 用法 ```bash -# Default model (Qwen/Qwen-Image-Edit-2509), square output -python3 {baseDir}/scripts/gen.py --prompt "a futuristic city at dusk" - -# Portrait / landscape sizes -python3 {baseDir}/scripts/gen.py --prompt "mountain lake" --image-size 720x1280 -python3 {baseDir}/scripts/gen.py --prompt "mountain lake" --image-size 1280x720 - -# Use Kolors model (supports guidance/batch) -python3 {baseDir}/scripts/gen.py --prompt "flower field" --model "Kwai-Kolors/Kolors" --batch-size 3 - -# Save to specific directory -python3 {baseDir}/scripts/gen.py --prompt "sunset" --out-dir ./out/images +# 1) 文生图(默认模型:Qwen/Qwen-Image) +python3 {baseDir}/scripts/gen.py --prompt "一只戴墨镜的柯基在海边" + +# 2) 文生图,输出 JPG 格式 +python3 {baseDir}/scripts/gen.py --prompt "一只戴墨镜的柯基在海边" --format jpg + +# 3) 改图(默认模型:Qwen/Qwen-Image-Edit-2509) +python3 {baseDir}/scripts/gen.py \ + --prompt "把这张图改成夜景霓虹风" \ + --image "https://example.com/original.jpg" + +# 4) 多参考图改图(按 API 支持可选传 image2/image3) +python3 {baseDir}/scripts/gen.py \ + --prompt "改成赛博朋克风格" \ + --image "https://example.com/a.jpg" \ + --image2 "https://example.com/b.jpg" \ + --image3 "https://example.com/c.jpg" + +# 5) 指定输出目录(可选) +python3 {baseDir}/scripts/gen.py --prompt "海报封面" --out-dir ./campaign_assets ``` -## Parameters - -| Flag | Default | Description | -|------|---------|-------------| -| `--prompt` | required | Text description for the image | -| `--model` | `Qwen/Qwen-Image-Edit-2509` | Model ID | -| `--image-size` | `1024x1024` | Resolution: `1024x1024`, `960x1280`, `768x1024`, `720x1440`, `720x1280` | -| `--batch-size` | `1` | Number of images (1–4, Kolors only) | -| `--steps` | `20` | Inference steps (1–100) | -| `--guidance` | — | Guidance scale (Kolors only) | -| `--negative-prompt` | — | What to avoid in the image | -| `--seed` | — | Random seed for reproducibility | -| `--out-dir` | `./tmp/sf-img-` | Output directory | - -## Output - -- `*.png` images named by index -- `prompts.json` mapping index → prompt + URL -- `index.html` thumbnail gallery - -## Environment Variables - -| Variable | Description | -|----------|-------------| -| `SILICONFLOW_API_KEY` | Your SiliconFlow API key (required) | +## 参数 + +| Flag | 默认值 | 说明 | +|------|--------|------| +| `--prompt` | 必填 | 文本描述 | +| `--model` | 自动选择 | 未传时:文生图=`Qwen/Qwen-Image`,改图=`Qwen/Qwen-Image-Edit-2509` | +| `--image` | — | 改图输入原图 URL;传入即进入改图模式 | +| `--image2` | — | 可选第二张参考图 URL | +| `--image3` | — | 可选第三张参考图 URL | +| `--image-size` | `1024x1024` | 图像分辨率 | +| `--steps` | `20` | 推理步数(`num_inference_steps`) | +| `--cfg` | `4` | 采样配置(`cfg`) | +| `--batch-size` | `1` | 批量生成数 | +| `--negative-prompt` | — | 负面提示词 | +| `--seed` | — | 随机种子 | +| `--format` | `jpg` | 输出格式:`png` / `jpg` / `jpeg` | +| `--out-dir` | `campaign_assets` | 输出根目录 | + +## 改图请求格式对齐说明 + +脚本已对齐以下字段: +- `model` +- `prompt` +- `num_inference_steps` +- `cfg` +- `image` +- `image2` +- `image3` + +等价于你提供的 SiliconFlow Images API 请求格式。 + +## 输出内容 + +每次执行会生成: +- `00.png`, `01.png`...(下载后的图片) +- `prompts.json`(prompt、模型、源图 URL、结果 URL) +- `index.html`(本地缩略图预览) diff --git a/crews/selfmedia-operator/skills/siliconflow-img-gen/scripts/gen.py b/crews/selfmedia-operator/skills/siliconflow-img-gen/scripts/gen.py index f2d7c624..6e9de450 100644 --- a/crews/selfmedia-operator/skills/siliconflow-img-gen/scripts/gen.py +++ b/crews/selfmedia-operator/skills/siliconflow-img-gen/scripts/gen.py @@ -1,41 +1,67 @@ #!/usr/bin/env python3 -"""SiliconFlow image generation — stdlib only (no httpx/requests).""" +"""SiliconFlow image generation/editing — stdlib only (no httpx/requests).""" import argparse import json import os import sys import time -import urllib.request import urllib.error +import urllib.request from pathlib import Path API_URL = "https://api.siliconflow.cn/v1/images/generations" -# Models that accept guidance_scale and batch_size -KOLORS_MODELS = {"kwai-kolors/kolors"} +DEFAULT_GEN_MODEL = "Qwen/Qwen-Image" +DEFAULT_EDIT_MODEL = "Qwen/Qwen-Image-Edit-2509" + + +def default_out_dir() -> Path: + """Return a migration-friendly default output directory. + + Resolve from this script location: + /skills/siliconflow-img-gen/scripts/gen.py + -> /campaign_assets + """ + workspace_root = Path(__file__).resolve().parents[3] + return workspace_root / "campaign_assets" + -def build_payload(args): - model = args.model +def build_payload(args, model: str): payload = { "model": model, "prompt": args.prompt, - "image_size": args.image_size, "num_inference_steps": args.steps, - "batch_size": args.batch_size, + "cfg": args.cfg, } - # guidance_scale only supported by Kolors - if args.guidance is not None: - if model.lower() in KOLORS_MODELS: - payload["guidance_scale"] = args.guidance - else: - print(f"[warn] --guidance ignored for model {model}", file=sys.stderr) + + # image_size is used for text-to-image. Edit models may ignore it silently. + payload["image_size"] = args.image_size + + # Output format (png/jpg) + output_format = args.format.lower() + if output_format == "jpeg": + output_format = "jpg" + if output_format in ["jpg", "jpeg"]: + payload["output_format"] = "jpeg" + if args.negative_prompt: payload["negative_prompt"] = args.negative_prompt if args.seed is not None: payload["seed"] = args.seed - # Qwen does not accept image_size or batch_size in some variants — keep them - # but note the model may ignore them silently + + # Edit mode accepts image/image2/image3 + if args.image: + payload["image"] = args.image + if args.image2: + payload["image2"] = args.image2 + if args.image3: + payload["image3"] = args.image3 + + # batch_size is useful for text generation variants. + if args.batch_size is not None: + payload["batch_size"] = args.batch_size + return payload @@ -65,17 +91,34 @@ def download_image(url, dest_path): dest_path.write_bytes(resp.read()) +def is_edit_mode(args) -> bool: + return bool(args.image) + + +def resolve_model(args) -> str: + if args.model: + return args.model + return DEFAULT_EDIT_MODEL if is_edit_mode(args) else DEFAULT_GEN_MODEL + + def main(): - parser = argparse.ArgumentParser(description="SiliconFlow image generation") + parser = argparse.ArgumentParser(description="SiliconFlow image generation/editing") parser.add_argument("--prompt", required=True) - parser.add_argument("--model", default="Qwen/Qwen-Image-Edit-2509") + parser.add_argument("--model", default=None, help="Model ID (auto by mode if omitted)") parser.add_argument("--image-size", default="1024x1024", dest="image_size") parser.add_argument("--batch-size", type=int, default=1, dest="batch_size") parser.add_argument("--steps", type=int, default=20) - parser.add_argument("--guidance", type=float, default=None) + parser.add_argument("--cfg", type=float, default=4) parser.add_argument("--negative-prompt", default=None, dest="negative_prompt") parser.add_argument("--seed", type=int, default=None) + + # Image-edit inputs. --image enables edit mode. + parser.add_argument("--image", default=None, help="Original image URL (required for edit mode)") + parser.add_argument("--image2", default=None, help="Optional second image URL") + parser.add_argument("--image3", default=None, help="Optional third image URL") + parser.add_argument("--out-dir", default=None, dest="out_dir") + parser.add_argument("--format", default="jpg", choices=["png", "jpg", "jpeg"], help="Output image format (default: jpg)") args = parser.parse_args() api_key = os.environ.get("SILICONFLOW_API_KEY") @@ -83,12 +126,19 @@ def main(): print("[error] SILICONFLOW_API_KEY not set", file=sys.stderr) sys.exit(1) + if (args.image2 or args.image3) and not args.image: + print("[error] --image2/--image3 require --image", file=sys.stderr) + sys.exit(1) + + model = resolve_model(args) + ts = int(time.time()) - out_dir = Path(args.out_dir) if args.out_dir else Path(f"./tmp/sf-img-{ts}") + out_root = Path(args.out_dir) if args.out_dir else default_out_dir() + out_dir = out_root / f"sf-img-{ts}" out_dir.mkdir(parents=True, exist_ok=True) - payload = build_payload(args) - print(f"[info] Generating image with model={args.model} size={args.image_size} …") + payload = build_payload(args, model) + print(f"[info] Requesting image(s): model={model} edit_mode={is_edit_mode(args)}") result = api_request(payload, api_key) images = result.get("images", []) @@ -96,20 +146,38 @@ def main(): print(f"[error] No images in response: {result}", file=sys.stderr) sys.exit(1) + # Determine output extension + output_format = args.format.lower() + if output_format == "jpeg": + output_format = "jpg" + ext = output_format + prompts_map = {} for i, img in enumerate(images): url = img.get("url", "") - dest = out_dir / f"{i:02d}.png" - print(f"[info] Downloading image {i} → {dest}") + dest = out_dir / f"{i:02d}.{ext}" + print(f"[info] Downloading image {i} -> {dest}") download_image(url, dest) - prompts_map[str(i)] = {"prompt": args.prompt, "url": url, "file": str(dest)} - - (out_dir / "prompts.json").write_text(json.dumps(prompts_map, ensure_ascii=False, indent=2)) + prompts_map[str(i)] = { + "prompt": args.prompt, + "url": url, + "file": str(dest), + "model": model, + "edit_mode": is_edit_mode(args), + "source_image": args.image or "", + "source_image2": args.image2 or "", + "source_image3": args.image3 or "", + } + + (out_dir / "prompts.json").write_text( + json.dumps(prompts_map, ensure_ascii=False, indent=2) + ) - # Simple HTML gallery gallery_html = [""] for i in range(len(images)): - gallery_html.append(f'') + gallery_html.append( + f'' + ) gallery_html.append("") (out_dir / "index.html").write_text("\n".join(gallery_html)) diff --git a/crews/selfmedia-operator/skills/siliconflow-video-gen/SKILL.md b/crews/selfmedia-operator/skills/siliconflow-video-gen/SKILL.md index a4e5d91c..416a6db4 100644 --- a/crews/selfmedia-operator/skills/siliconflow-video-gen/SKILL.md +++ b/crews/selfmedia-operator/skills/siliconflow-video-gen/SKILL.md @@ -1,78 +1,61 @@ --- name: siliconflow-video-gen -description: Generate videos via SiliconFlow Video API. Supports text-to-video (T2V) and image-to-video (I2V) using Wan2.2 models. Async: submit job → poll until done → download. +description: Generate videos via SiliconFlow Video API. Supports text-to-video (T2V) and image-to-video (I2V) using Wan2.2 models. Async flow: submit job, poll status, then download. Default output path resolves to workspace campaign_assets via relative path logic for portability. homepage: https://docs.siliconflow.cn/cn/userguide/capabilities/video metadata: { "openclaw": { "emoji": "🎬", - "requires": { "bins": ["python3"], "env": ["SILICONFLOW_API_KEY"] }, - "primaryEnv": "SILICONFLOW_API_KEY", + "requires": { "bins": ["python3"] }, }, } --- # SiliconFlow Video Gen -Generate videos using the SiliconFlow Video API (Wan2.2 models). +通过 SiliconFlow Video API 生成视频(Wan2.2 系列)。 -Video generation is **asynchronous**: the API returns a `requestId` immediately, then the script polls the status endpoint until the job completes (status: `Succeed`). +## 默认输出位置 -> The generated video URL is valid for **1 hour**. The script downloads the video locally automatically. +默认输出到当前 agent workspace 下的 `campaign_assets/sf-video-/`。 -## Run +> 脚本通过相对路径推导 workspace 根目录,不写死绝对路径,便于后续迁移。 -Note: Video generation typically takes **1–5 minutes**. Set exec timeout accordingly (e.g., `exec timeout=600`). +## 运行示例 ```bash -# Text-to-video -python3 {baseDir}/scripts/gen.py --prompt "a dolphin leaping over ocean waves at sunset" +# 文生视频(T2V) +python3 {baseDir}/scripts/gen.py --prompt "海边日落延时摄影" -# Image-to-video (provide a public URL or local base64 image) +# 图生视频(I2V) python3 {baseDir}/scripts/gen.py \ --model "Wan-AI/Wan2.2-I2V-A14B" \ - --prompt "the camera slowly zooms out" \ + --prompt "镜头缓慢拉远" \ --image "https://example.com/my-photo.jpg" -# Custom resolution and output directory +# 指定分辨率与输出目录 python3 {baseDir}/scripts/gen.py \ - --prompt "time-lapse of a blooming flower" \ + --prompt "城市夜景" \ --image-size 720x1280 \ - --out-dir ./out/videos - -# Reproducible generation with a fixed seed -python3 {baseDir}/scripts/gen.py --prompt "rocket launch" --seed 42 + --out-dir ./campaign_assets ``` -## Parameters +## 参数 -| Flag | Default | Description | +| Flag | 默认值 | 说明 | |------|---------|-------------| -| `--prompt` | required | Text description of the video | -| `--model` | `Wan-AI/Wan2.2-T2V-A14B` | Model ID: `Wan-AI/Wan2.2-T2V-A14B` (T2V) or `Wan-AI/Wan2.2-I2V-A14B` (I2V) | -| `--image` | — | Image URL or `data:image/...;base64,...` (required for I2V model) | -| `--image-size` | `1280x720` | Resolution: `1280x720` (16:9), `720x1280` (9:16), `960x960` (1:1) | -| `--negative-prompt` | — | What to avoid in the video | -| `--seed` | — | Random seed for reproducibility | -| `--poll-interval` | `10` | Seconds between status polls | -| `--timeout` | `600` | Max seconds to wait for generation | -| `--out-dir` | `./tmp/sf-video-` | Output directory | - -## Models - -| Model | Type | Notes | -|-------|------|-------| -| `Wan-AI/Wan2.2-T2V-A14B` | Text → Video | Default model | -| `Wan-AI/Wan2.2-I2V-A14B` | Image → Video | Requires `--image` parameter | - -## Output - -- `video_.mp4` downloaded locally -- `result.json` with full API response - -## Environment Variables - -| Variable | Description | -|----------|-------------| -| `SILICONFLOW_API_KEY` | Your SiliconFlow API key (required) | +| `--prompt` | 必填 | 视频描述 | +| `--model` | `Wan-AI/Wan2.2-T2V-A14B` | 模型:T2V 或 I2V | +| `--image` | — | I2V 模式必填:图片 URL 或 base64 data URI | +| `--image-size` | `1280x720` | 分辨率:`1280x720` / `720x1280` / `960x960` | +| `--negative-prompt` | — | 负面提示词 | +| `--seed` | — | 随机种子 | +| `--poll-interval` | `10` | 轮询间隔(秒) | +| `--timeout` | `600` | 最长等待时长(秒) | +| `--out-dir` | `campaign_assets` | 输出根目录 | + +## 输出 + +- `video_.mp4` +- `result.json`(完整 API 返回) diff --git a/crews/selfmedia-operator/skills/siliconflow-video-gen/scripts/gen.py b/crews/selfmedia-operator/skills/siliconflow-video-gen/scripts/gen.py index c04943a5..2c89c7dc 100644 --- a/crews/selfmedia-operator/skills/siliconflow-video-gen/scripts/gen.py +++ b/crews/selfmedia-operator/skills/siliconflow-video-gen/scripts/gen.py @@ -24,6 +24,17 @@ VALID_SIZES = {"1280x720", "720x1280", "960x960"} +def default_out_dir() -> Path: + """Return a migration-friendly default output directory. + + Resolve from this script location: + /skills/siliconflow-video-gen/scripts/gen.py + -> /campaign_assets + """ + workspace_root = Path(__file__).resolve().parents[3] + return workspace_root / "campaign_assets" + + def post_json(url, payload, api_key, timeout=60): data = json.dumps(payload).encode() req = urllib.request.Request( @@ -75,7 +86,7 @@ def poll_until_done(request_id, api_key, poll_interval, timeout): def download_video(url, dest_path): """Stream-download the video file to dest_path.""" - print(f"[info] Downloading video → {dest_path}") + print(f"[info] Downloading video -> {dest_path}") req = urllib.request.Request(url, headers={"User-Agent": "wiseflow-video-gen/1.0"}) with urllib.request.urlopen(req, timeout=300) as resp: dest_path.write_bytes(resp.read()) @@ -119,7 +130,8 @@ def main(): sys.exit(1) ts = int(time.time()) - out_dir = Path(args.out_dir) if args.out_dir else Path(f"./tmp/sf-video-{ts}") + out_root = Path(args.out_dir) if args.out_dir else default_out_dir() + out_dir = out_root / f"sf-video-{ts}" out_dir.mkdir(parents=True, exist_ok=True) payload = { diff --git a/crews/selfmedia-operator/skills/wenyan-formatter/SKILL.md b/crews/selfmedia-operator/skills/wenyan-formatter/SKILL.md deleted file mode 100644 index 3f121953..00000000 --- a/crews/selfmedia-operator/skills/wenyan-formatter/SKILL.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -name: wenyan-formatter -description: Format Markdown drafts into styled HTML for preview, or publish directly to WeChat Official Account (GZH) draft box. Supports multiple visual themes with smart topic-based theme selection. -homepage: https://github.com/theajack/wenyan -metadata: - { - "openclaw": - { - "emoji": "📝", - "requires": { "bins": ["node", "npx"] }, - "optionalEnv": ["WECHAT_APP_ID", "WECHAT_APP_SECRET"], - }, - } ---- - -# Wenyan Formatter - -将 Markdown 草稿渲染为微信公众号风格的 HTML,或一键推送到公众号草稿箱。 - -## Run - -### 预览(render) - -```bash -# 渲染本地 Markdown 文件 -bash {baseDir}/scripts/format.sh --file ./draft.md - -# 渲染内联 Markdown,指定主题 -bash {baseDir}/scripts/format.sh --content "## 标题\n\n正文内容..." --theme grace - -# 指定代码高亮主题 -bash {baseDir}/scripts/format.sh --file ./draft.md --theme tech --highlight github-dark - -# 自定义 CSS 主题 -bash {baseDir}/scripts/format.sh --file ./draft.md --custom-theme ./my-theme.css -``` - -### 发布到公众号草稿箱(publish) - -> 需要预先设置 `WECHAT_APP_ID` 和 `WECHAT_APP_SECRET`。 - -```bash -# 本机 IP 已在公众号 IP 白名单时,直接发布 -bash {baseDir}/scripts/format.sh --action publish --file ./draft.md --theme grace - -# 通过 Wenyan Server 绕过 IP 白名单限制(推荐) -bash {baseDir}/scripts/format.sh \ - --action publish \ - --file ./draft.md \ - --theme grace \ - --server https://your-wenyan-server.example.com \ - --api-key YOUR_SERVER_KEY -``` - -## Parameters - -| Flag | Default | Description | -|------|---------|-------------| -| `--action` | `render` | `render`(仅输出 HTML)或 `publish`(推送草稿) | -| `--file` | — | 本地 Markdown 文件路径(与 `--content` 二选一) | -| `--content` | — | 直接传入 Markdown 字符串 | -| `--theme` | `default` | 视觉主题 ID(见下方主题列表) | -| `--highlight` | `solarized-light` | 代码块高亮主题 | -| `--custom-theme` | — | 自定义 CSS 文件路径 | -| `--no-mac-style` | — | 关闭 Mac 风格代码块头部 | -| `--no-footnote` | — | 禁用链接转脚注 | -| `--out-dir` | `./tmp/wenyan-` | 渲染输出���录(仅 render 模式) | -| `--server` | — | Wenyan Server URL(publish 模式,绕过 IP 白名单) | -| `--api-key` | — | Wenyan Server API Key(配合 `--server` 使用) | - -## Output - -**render 模式** - -- `/output.html` — 可在浏览器预览的完整 HTML 页面 -- `/source.md` — 原始 Markdown 备份 - -**publish 模式** - -- 控制台打印推送结果(草稿 media_id 或错误信息) - -## Theme List & 智能主题匹配 - -> Agent 应根据文章内容自动选择最贴切的主题,避免每次都使用默认主题。 - -| 主题 ID | 视觉风格 | 适用内容场景 | -|---------|---------|------------| -| `default` | 简洁黑白,标准微信排版 | 通知、简讯、时效性资讯 | -| `grace` | 优雅衬线,米白底色 | 深度长文、读书笔记、文化艺术 | -| `tech` | 深色背景,等宽字体强调 | 技术教程、代码分析、产品评测 | -| `fresh` | 清新绿色调,轻量感 | 健康生活、美食、户外、轻量资讯 | -| `warm` | 暖橙/土黄,温暖感 | 情感、故事、节日、生活感悟 | -| `elegant` | 极简留白,高对比标题 | 品牌内容、商务、高端产品推介 | -| `cute` | 粉色圆角,活泼插画感 | 亲子、宠物、娱乐、年轻用户 | - -### 智能主题匹配决策树 - -``` -内容含大量代码 / 技术术语? - └─ 是 → tech - -主要受众是年轻女性 / 亲子 / 萌宠? - └─ 是 → cute - -情感/故事/节日/生活类内容? - └─ 是 → warm - -健康 / 美食 / 户外 / 轻量生活? - └─ 是 → fresh - -品牌推广 / 商务 / 精品内容? - └─ 是 → elegant - -深度长文 / 文化 / 书评? - └─ 是 → grace - -以上均不符合(资讯/通知/简讯) - └─ → default -``` - -> 用户明确指定主题时,遵从用户选择;不确定时按决策树匹配,并在消息中告知已选主题及原因。 - -## Environment Variables - -| 变量 | 说明 | -|------|------| -| `WECHAT_APP_ID` | 微信公众号 AppID(publish 模式必填) | -| `WECHAT_APP_SECRET` | 微信公众号 AppSecret(publish 模式必填) | - -## Notes - -- 第一次运行 `npx @wenyan-md/cli` 时会自动下载包,耗时约 10–30 秒,之后有缓存 -- **publish 模式**默认要求本机 IP 在公众号后台 IP 白名单中;生产环境建议配置 Wenyan Server 绕过限制 -- Markdown 文件应包含 YAML frontmatter(至少含 `title:` 字段),否则 publish 时可能被微信 API 拒绝 -- 渲染 HTML 仅供预览,最终排版以公众号后台实际效果为准 diff --git a/crews/selfmedia-operator/skills/wenyan-formatter/scripts/format.sh b/crews/selfmedia-operator/skills/wenyan-formatter/scripts/format.sh deleted file mode 100644 index 67219385..00000000 --- a/crews/selfmedia-operator/skills/wenyan-formatter/scripts/format.sh +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env bash -# wenyan-formatter — Markdown → styled HTML (render) or WeChat GZH draft (publish) -# Wraps @wenyan-md/cli via npx; requires Node.js 18+. -# -# Usage: -# bash format.sh [options] -# -# Actions: -# render (default) — convert Markdown to styled HTML, save to --out-dir -# publish — render + upload images + push to WeChat GZH draft box -# -# Input (one required): -# --file local Markdown file path -# --content inline Markdown string (will be piped via stdin) -# -# Options: -# --action default: render -# --theme theme ID (see SKILL.md for list); default: default -# --highlight code highlight theme; default: solarized-light -# --custom-theme path to custom CSS file -# --no-mac-style disable Mac-style code block header -# --no-footnote disable link→footnote conversion -# --out-dir output directory (render only); default: ./tmp/wenyan- -# --server Wenyan Server URL (publish only, bypasses IP whitelist) -# --api-key API key for Wenyan Server (publish only) -# -# Environment (publish only): -# WECHAT_APP_ID WeChat GZH app ID -# WECHAT_APP_SECRET WeChat GZH app secret -set -euo pipefail - -# ---------- defaults ---------- -ACTION="render" -FILE="" -CONTENT="" -THEME="default" -HIGHLIGHT="solarized-light" -CUSTOM_THEME="" -OUT_DIR="" -SERVER="" -API_KEY="" -MAC_STYLE_FLAG="--mac-style" -FOOTNOTE_FLAG="--footnote" - -# ---------- argument parsing ---------- -while [[ $# -gt 0 ]]; do - case "$1" in - --action) ACTION="$2"; shift 2 ;; - --file) FILE="$2"; shift 2 ;; - --content) CONTENT="$2"; shift 2 ;; - --theme) THEME="$2"; shift 2 ;; - --highlight) HIGHLIGHT="$2"; shift 2 ;; - --custom-theme) CUSTOM_THEME="$2"; shift 2 ;; - --out-dir) OUT_DIR="$2"; shift 2 ;; - --server) SERVER="$2"; shift 2 ;; - --api-key) API_KEY="$2"; shift 2 ;; - --no-mac-style) MAC_STYLE_FLAG="--no-mac-style"; shift ;; - --no-footnote) FOOTNOTE_FLAG="--no-footnote"; shift ;; - *) - echo "[error] Unknown argument: $1" >&2 - exit 1 - ;; - esac -done - -# ---------- validate action ---------- -if [[ "$ACTION" != "render" && "$ACTION" != "publish" ]]; then - echo "[error] --action must be 'render' or 'publish', got: $ACTION" >&2 - exit 1 -fi - -# ---------- validate input ---------- -if [[ -z "$FILE" && -z "$CONTENT" ]]; then - echo "[error] Provide either --file or --content " >&2 - exit 1 -fi - -if [[ -n "$FILE" && ! -f "$FILE" ]]; then - echo "[error] File not found: $FILE" >&2 - exit 1 -fi - -# ---------- check node/npx ---------- -if ! command -v node &>/dev/null; then - echo "[error] node not found. Install Node.js 18+ first." >&2 - exit 1 -fi - -NODE_VER=$(node -e "process.stdout.write(process.version)") -NODE_MAJOR=$(echo "$NODE_VER" | sed 's/v//' | cut -d. -f1) -if [[ "$NODE_MAJOR" -lt 18 ]]; then - echo "[error] Node.js 18+ required, found $NODE_VER" >&2 - exit 1 -fi - -if ! command -v npx &>/dev/null; then - echo "[error] npx not found. It should ship with Node.js." >&2 - exit 1 -fi - -# ---------- publish: pre-flight checks ---------- -if [[ "$ACTION" == "publish" ]]; then - if [[ -z "${WECHAT_APP_ID:-}" || -z "${WECHAT_APP_SECRET:-}" ]]; then - echo "[error] publish requires WECHAT_APP_ID and WECHAT_APP_SECRET to be set." >&2 - echo "[hint] Set them via environment variables before calling this script." >&2 - exit 1 - fi - - # frontmatter title check (only when using --content, file is user's responsibility) - if [[ -n "$CONTENT" ]]; then - if ! echo "$CONTENT" | grep -qE '^---' || ! echo "$CONTENT" | grep -qE '^title:'; then - echo "[warn] publish mode: --content should include frontmatter with 'title:' field." >&2 - echo "[warn] Missing title may cause WeChat API rejection." >&2 - fi - fi -fi - -# ---------- build npx args ---------- -WENYAN_ARGS=("--yes" "@wenyan-md/cli" "$ACTION") - -if [[ -n "$FILE" ]]; then - WENYAN_ARGS+=("--file" "$FILE") -fi - -WENYAN_ARGS+=("--theme" "$THEME") -WENYAN_ARGS+=("--highlight" "$HIGHLIGHT") -WENYAN_ARGS+=("$MAC_STYLE_FLAG") -WENYAN_ARGS+=("$FOOTNOTE_FLAG") - -if [[ -n "$CUSTOM_THEME" ]]; then - WENYAN_ARGS+=("--custom-theme" "$CUSTOM_THEME") -fi - -if [[ "$ACTION" == "publish" ]]; then - if [[ -n "$SERVER" ]]; then - WENYAN_ARGS+=("--server" "$SERVER") - if [[ -n "$API_KEY" ]]; then - WENYAN_ARGS+=("--api-key" "$API_KEY") - fi - fi -fi - -# ---------- render: prepare output directory ---------- -if [[ "$ACTION" == "render" ]]; then - TS=$(date +%s) - if [[ -z "$OUT_DIR" ]]; then - OUT_DIR="./tmp/wenyan-${TS}" - fi - mkdir -p "$OUT_DIR" - HTML_FILE="${OUT_DIR}/output.html" - - echo "[info] Action: render | Theme: ${THEME} | Highlight: ${HIGHLIGHT}" - echo "[info] Output dir: ${OUT_DIR}" - - # Run wenyan render → capture HTML output - if [[ -n "$CONTENT" ]]; then - HTML_CONTENT=$(echo "$CONTENT" | npx "${WENYAN_ARGS[@]}") - else - HTML_CONTENT=$(npx "${WENYAN_ARGS[@]}") - fi - - # Wrap in minimal page for browser preview - cat > "$HTML_FILE" < - - - - -wenyan preview - - - -${HTML_CONTENT} - - -HTML_EOF - - # Also save original Markdown for reference - if [[ -n "$FILE" ]]; then - cp "$FILE" "${OUT_DIR}/source.md" - echo "[done] Source: ${OUT_DIR}/source.md" - else - echo "$CONTENT" > "${OUT_DIR}/source.md" - echo "[done] Source: ${OUT_DIR}/source.md" - fi - - echo "[done] HTML: ${HTML_FILE}" - echo "[done] Theme used: ${THEME}" - -# ---------- publish ---------- -else - echo "[info] Action: publish | Theme: ${THEME}" - if [[ -n "$SERVER" ]]; then - echo "[info] Using server mode: ${SERVER}" - else - echo "[info] Using local mode (IP must be in WeChat whitelist)" - fi - - if [[ -n "$CONTENT" ]]; then - RESULT=$(echo "$CONTENT" | npx "${WENYAN_ARGS[@]}") - else - RESULT=$(npx "${WENYAN_ARGS[@]}") - fi - - echo "[done] $RESULT" -fi diff --git a/docs/quick_start.md b/docs/quick_start.md index 69633129..241e1efa 100644 --- a/docs/quick_start.md +++ b/docs/quick_start.md @@ -1,80 +1,168 @@ # 部署后怎么用(推荐流程) -### 1) 最小可用:只配置 Main Agent 一个 channel +### 推荐上手三步走 -最小配置可以只接入 `main`(例如只配置一个飞书机器人),先让所有请求都进 Main,再由 Main 分发给其他 Crew。 +以下三个场景是最典型的初始配置流程,对话内容可直接照抄。 -你可以在消息里直接用 **强制路由前缀** 指定处理人: +--- + +#### 第一步:通过 Main Agent 招募对内 Crew(以新媒体小编为例) + +> 适用 crew 类型:**internal**(在 Main Agent 的频道中操作) + +**1.1 招募新媒体小编** + +```text +你(在 main 频道): +帮我招募一个新媒体小编,用 selfmedia-operator 模板,ID 就叫 selfmedia-operator + +# Main Agent 会展示招募方案,等你确认 +你:确认 + +# Main Agent 完成实例化,提示重启 Gateway +``` + +**1.2 向小编植入业务背景** + +实例化完成、重启 Gateway 后,告诉 Main Agent 向小编注入背景信息(或者等单独绑定小编 channel 后,直接跟小编说): ```text -@it-engineer 帮我检查 gateway 日志 -@hrbp 帮我招聘一个客服 crew +你: +帮我把以下背景信息注入新媒体小编的工作记忆: +- 公司:<你的公司名称>,做 <主营业务一句话> +- 运营平台:微信公众号 <账号名> + 小红书 +- 账号调性:<风格定位,如”技术圈硬核但不失活泼”> +- 目标读者:<典型受众描述> +- 发布节奏:每周 2 篇,周二和周五 +- 禁忌话题:<不可提及的竞品或话题> + +# Main Agent 会更新 selfmedia-operator 的 MEMORY.md 和 USER.md ``` -也支持完整写法: +**1.3 开始使用** ```text -[Route: @it-engineer] 帮我检查 gateway 日志 +你(在 main 频道,直接路由): +@selfmedia-operator 帮我写一篇关于 <选题> 的文章,面向 <目标读者> + +# 或者给新媒体小编绑定独立频道后直接对话 ``` -### 2) 先让 IT Engineer 帮你完成系统配置 +--- -部署后建议先找 `it-engineer` 做基础配置和巡检,例如: -- 模型/API 配置检查 -- channel 连接状态检查 -- 日志排查与升级建议 -- 日常维护操作(重启、更新、故障恢复) +#### 第二步:通过 HRBP 招募对外 Crew(以销售客服为例) -### 3) 通过 HRBP 招募新 Crew +> 适用 crew 类型:**external**(在 HRBP 的频道中操作) -`hrbp` 是唯一的生命周期管理入口(招聘、调岗、停用)。推荐直接描述你的业务目标,例如: +**2.1 告诉 HRBP 你的业务背景和需求** ```text -@hrbp 我需要一个“短视频运营”crew,先不绑定独立 channel,走 Main 分发 +你(在 hrbp 频道): +我需要招募一个微信销售客服,基于 sales-cs 模板。 +业务背景如下: +- 公司:<你的公司名称> +- 产品:<核心产品/服务名称>,<一句话说清楚能帮客户解决什么> +- 付费层级:<例如:免费体验版 → VIP会员 → 企业订阅> +- 客服对外称呼:<如”小明助手”> +- 负责人微信:<负责人微信号>(客户复杂问题时升级人工) +- 绑定渠道:飞书,账号 ID ``` -HRBP 会完成模板匹配/实例化/注册,并把结果同步到团队通讯录。 +**2.2 HRBP 配置实例并引导你填写业务内容** + +```text +# HRBP 会展示实例化方案,等你确认 +你:确认 + +# HRBP 完成实例化,并请你提供以下内容填写到客服手册: +# - 产品 FAQ(常见问题答案) +# - 付费方式和购买链接 +# - 反馈问卷链接 +# - 开票申请工单链接 + +你: +产品常见问题如下: +Q:<问题1>?A:<答案1> +Q:<问题2>?A:<答案2> +购买链接:<链接> +反馈问卷:<链接> +开票工单:<链接> + +# HRBP 把这些内容写入 sales-cs 实例的 MEMORY.md +``` + +**注意:对外 crew 必须单独绑定对外服务 channel,推荐使用代码仓中的 awada extension** + +**2.3 重启 Gateway,销售客服上线** + +```text +你:确认,帮我重启 Gateway + +# 或在终端手动执行: +# ./scripts/dev.sh gateway +``` -### 4) 常用 Crew 建议单独绑定 channel(推荐) +--- -最小模式(只配 Main)适合起步;当某些 Crew 进入高频使用后,建议给它们单独绑定 channel: -- 好处:沟通更直接、上下文更稳定、减少 Main 的中转噪音 -- 同时保留 Main 分发能力(`spawn` + `binding` 可共存) +#### 第三步:通过 IT Engineer 日常运维 -### 5) 查看团队通讯录 +> IT Engineer 是内置的系统级 crew,无需招募,通过 Main Agent 频道路由 -部署后系统会自动维护 `~/.openclaw/TEAM_DIRECTORY.md`,记录当前启用 Crew 的: -- ID -- 名称 -- 职责(从 IDENTITY.md 提取) -- 路由方式(spawn/binding/both) -- 绑定渠道 +**3.1 系统巡检** -### 6) 3 分钟上手示例对话(可直接照抄) +```text +你(在 main 频道): +@it-engineer 帮我做一次系统巡检,重点检查: +- 模型/API 配置是否正常 +- 各 crew 的 channel 绑定状态 +- 最近有没有异常日志 +``` -如下都是你发给 main agent 的: +**3.2 升级系统** ```text -你:@it-engineer 帮我检查当前配置是否可用,重点看模型和飞书连接 +你:@it-engineer 帮我把系统升级到最新版本 -你:@hrbp 我需要一个“短视频运营”crew,先不绑定独立 channel,走 Main 分发 +# IT Engineer 会执行 ./scripts/upgrade.sh 并报告结果 +``` -你:@main 把今天要发的短视频选题交给短视频运营 crew +**3.3 排查故障** + +```text +你: +@it-engineer 新媒体小编今天回复说”工具调用失败”,帮我查一下原因 + +# IT Engineer 会检查日志、配置,定位问题并修复 +``` -你:@hrbp 给 short-video-ops 绑定一个单独的飞书账号 short-video-bot +**3.4 查看团队通讯录** -你:@short-video-ops 以后你直接负责我的短视频选题、脚本和发布时间建议 +```text +你:帮我看一下现在团队里有哪些 crew,以及各自的绑定状态 -你:@it-engineer 帮我确认 TEAM_DIRECTORY.md 里 short-video-ops 的路由状态和绑定是否正确 +# Main Agent 会读取 TEAM_DIRECTORY.md,输出当前团队状态 ``` -说明: -- 第 1 句先让 IT Engineer 做基础体检 -- 第 2 句由 HRBP 招聘新 crew(生命周期变更只走 HRBP) -- 第 3 句在 Main 模式下调度新 crew -- 第 4 句由 HRBP 给新 crew 绑定独立 channel,升级为”直连”模式 -- 第 5 句仍然发到 Main channel 中,由 Main 路由到 short-video-ops(如需跳过 Main 中转,请使用绑定后的独立 channel 直接对话) -- 第 6 句用 IT Engineer 做最终核验 +--- + +#### 3 分钟速查(可直接照抄) + +```text +# 招募对内 crew(在 main 频道) +你:帮我招募一个新媒体小编,用 selfmedia-operator 模板 + +# 注入业务背景(在 main 频道) +你:帮我把以下背景注入新媒体小编:[业务背景...] + +# 招募对外 crew(在 hrbp 频道) +你:我需要招募一个微信销售客服,基于 sales-cs 模板,[业务背景...] + +# 系统巡检(在 main 频道路由) +你:@it-engineer 做一次系统巡检 + +# 查看团队状态 +你:帮我看一下现在有哪些 crew 在运行 +``` # 生产部署 diff --git a/scripts/lib/agent-skills.sh b/scripts/lib/agent-skills.sh index 4752f1e0..0600e240 100644 --- a/scripts/lib/agent-skills.sh +++ b/scripts/lib/agent-skills.sh @@ -272,6 +272,22 @@ console.log(JSON.stringify(Array.from(new Set(lines)))); ' } +# 向 external crew 的 AGENTS.md 追加渠道回复规则(幂等) +# 规则:调用工具的 turn 不得包含面向客户的文本,所有文本在最后一个 turn 统一输出 +inject_channel_reply_rules() { + local agents_md="$1" + [ -f "$agents_md" ] || return 0 + grep -qF "## 渠道回复规则(自动注入)" "$agents_md" && return 0 + cat >> "$agents_md" << 'RULES' + +--- + +## 渠道回复规则(自动注入) + +调用任何工具(exec / message / read 等)的 turn 中,不得包含任何面向客户的文本。面向客户的完整回复必须在所有工具执行完成后,在最后一个 turn 中统一输出。违反此规则会导致客户收到多条内容相近的消息。 +RULES +} + # 向 workspace 的 TOOLS.md 追加通用工具调用规范(幂等) # 注入内容见 docs/injected_instruction.md inject_file_edit_guide() { diff --git a/scripts/setup-crew.sh b/scripts/setup-crew.sh index a3a7b589..ed2e483f 100755 --- a/scripts/setup-crew.sh +++ b/scripts/setup-crew.sh @@ -287,6 +287,13 @@ if [ -f "$CREWS_DIR/hrbp_index.md" ]; then fi echo " ✅ External crew templates synced to $HRBP_TEMPLATES_DEST" +# ─── 3c. 注入渠道回复规则到所有对外 crew 模板 ──────────────────── +for template_dir in "$HRBP_TEMPLATES_DEST"/*/; do + [ -d "$template_dir" ] || continue + inject_channel_reply_rules "$template_dir/AGENTS.md" +done +echo " ✅ Channel reply rules injected into external crew templates" + # ─── 4. 更新 openclaw.json(合并内置 Crew + skills 过滤) ──────── if [ -f "$CONFIG_PATH" ]; then echo " 📝 Merging agent config into openclaw.json..." @@ -422,7 +429,7 @@ if [ -f "$CONFIG_PATH" ]; then name: prev.name || 'Main Agent', workspace: prev.workspace || openclawHome + '/workspace-main', thinkingDefault: 'high', - reasoningDefault: 'on', + reasoningDefault: 'off', subagents: { ...(prev.subagents || {}), allowAgents: allowAgents, @@ -439,7 +446,7 @@ if [ -f "$CONFIG_PATH" ]; then name: prev.name || 'HRBP', workspace: prev.workspace || openclawHome + '/workspace-hrbp', thinkingDefault: 'high', - reasoningDefault: 'on', + reasoningDefault: 'off', subagents: { ...(prev.subagents || {}), allowAgents: ['it-engineer'], @@ -455,7 +462,7 @@ if [ -f "$CONFIG_PATH" ]; then name: prev.name || 'IT Engineer', workspace: prev.workspace || openclawHome + '/workspace-it-engineer', thinkingDefault: 'high', - reasoningDefault: 'on', + reasoningDefault: 'off', }; return applySkills(base, process.env.IT_SKILLS_RESULT); }); @@ -477,7 +484,7 @@ if [ -f "$CONFIG_PATH" ]; then } // 对内 crew 默认思考/推理设置(不覆盖已有配置) if (!agent.thinkingDefault) agent.thinkingDefault = 'medium'; - if (!agent.reasoningDefault) agent.reasoningDefault = 'on'; + if (!agent.reasoningDefault) agent.reasoningDefault = 'off'; } else { // 对外 crew 默认思考/推理设置(不覆盖已有配置) if (!agent.thinkingDefault) agent.thinkingDefault = 'medium'; @@ -542,6 +549,26 @@ if [ -f "$CONFIG_PATH" ]; then echo " 📝 Applying command tier exec policies..." EXEC_APPROVALS_PATH="$OPENCLAW_HOME/exec-approvals.json" apply_exec_tiers "$CONFIG_PATH" "$EXEC_APPROVALS_PATH" "$CREWS_DIR" "$PROJECT_ROOT" + + # ─── 4d. 注入渠道回复规则到已部署的对外 crew workspaces ────── + while IFS=$'\t' read -r a_id a_ws; do + [ -n "$a_id" ] || continue + [ -f "$a_ws/SOUL.md" ] || continue + [ "$(resolve_crew_type "$a_ws/SOUL.md")" = "external" ] || continue + inject_channel_reply_rules "$a_ws/AGENTS.md" + done < <(node -e " + const fs = require('fs'); + const home = process.env.HOME || ''; + const c = JSON.parse(fs.readFileSync('$CONFIG_PATH', 'utf8')); + for (const a of (c.agents?.list || [])) { + if (!a?.id) continue; + const ws = (typeof a.workspace === 'string' && a.workspace.trim() + ? a.workspace.trim() : ('~/.openclaw/workspace-' + a.id)) + .replace(/^~(?=\/|\$)/, home); + console.log(a.id + '\t' + ws); + } + " 2>/dev/null) + echo " ✅ Channel reply rules synced to deployed external crew workspaces" else echo " ⚠️ openclaw.json not found at $CONFIG_PATH" echo " Will be created on first start (dev.sh / reinstall-daemon.sh)"