@clawhub-gavinyao-e61fb87b24
长桥交易助手。通过 longport Python SDK 帮助用户查询实时行情、K线、盘口,提交/修改/撤销订单,查看账户余额和持仓。当用户提到股票行情、买入、卖出、下单、持仓、账户余额、订单、K线、盘口、成交记录、现金流等与证券交易相关的操作时,必须使用此 skill。即使用户只是随口问「AAPL 多少钱」或...
---
name: longbridge-trader
description: 长桥交易助手。通过 longport Python SDK 帮助用户查询实时行情、K线、盘口,提交/修改/撤销订单,查看账户余额和持仓。当用户提到股票行情、买入、卖出、下单、持仓、账户余额、订单、K线、盘口、成交记录、现金流等与证券交易相关的操作时,必须使用此 skill。即使用户只是随口问「AAPL 多少钱」或「我账户里还有多少钱」也应触发。
---
# 长桥交易助手
通过 longport Python SDK 帮助用户完成港股、美股的行情查询和交易操作。
## 环境要求
- Python 包:`longport`(通过 `pip install longport` 安装)
- 环境变量(SDK 通过 `Config.from_env()` 自动读取):
- `LONGPORT_APP_KEY`
- `LONGPORT_APP_SECRET`
- `LONGPORT_ACCESS_TOKEN`
如果用户未配置环境变量,提示他们到 [长桥开放平台](https://open.longbridge.cn) 获取。
## 标的代码格式
所有标的使用 `{市场}.{代码}` 格式:
| 市场 | 前缀 | 示例 |
|------|------|------|
| 美股 | `US` | `US.AAPL`, `US.TSLA` |
| 港股 | `HK` | `HK.00700`, `HK.09988` |
| A股(沪) | `SH` | `SH.600519` |
| A股(深) | `SZ` | `SZ.000001` |
## 标的代码补全
用户经常省略市场前缀或直接用中文名称,需要自动补全为完整的 `{市场}.{代码}` 格式:
1. **纯英文字母**(如 `AAPL`、`TSLA`)→ 美股,补全为 `US.AAPL`
2. **5位数字**(如 `00700`、`09988`)→ 港股,补全为 `HK.00700`
3. **6位数字,6开头**(如 `600519`)→ 沪市,补全为 `SH.600519`
4. **6位数字,0/3开头**(如 `000001`、`300750`)→ 深市,补全为 `SZ.000001`
5. **中文名称**(如「腾讯」「苹果」「茅台」)→ 根据常识映射到对应标的代码;不确定时询问用户确认
## 安全规则
**下单、改单操作必须遵循以下流程:**
1. 先向用户展示完整的订单参数(标的、方向、类型、数量、价格)
2. 明确询问用户「确认下单吗?」
3. 收到用户确认后才执行
**禁止**:在没有用户明确确认的情况下执行任何买入/卖出/改单操作。查询类操作(行情、持仓、余额)无需确认,直接执行。
## 工作流程
根据用户意图选择对应工作流。所有代码通过 Bash 工具执行 `python3 -c "..."` 运行。
### 1. 查询行情
获取股票实时报价、K线、盘口等数据。
```python
from longport.openapi import QuoteContext, Config
config = Config.from_env()
ctx = QuoteContext(config)
# 实时行情
resp = ctx.quote(["US.AAPL"])
for q in resp:
print(f"{q.symbol} 最新: {q.last_done} 涨跌: {q.change_rate}%")
# K线(最近 30 根日K)
from longport.openapi import Period, AdjustType
candles = ctx.candlesticks("US.AAPL", Period.Day, 30, AdjustType.ForwardAdjust)
# 盘口
depth = ctx.depth("US.AAPL")
# 分时
intraday = ctx.intraday("US.AAPL")
```
有关 QuoteContext 完整方法列表和参数说明,阅读 `references/quote-api.md`。
### 2. 交易操作
提交、修改、撤销订单。
```python
from longport.openapi import TradeContext, Config, OrderType, OrderSide, TimeInForceType, Decimal
config = Config.from_env()
ctx = TradeContext(config)
# 提交限价单:买入 100 股 AAPL,价格 150.00
resp = ctx.submit_order(
symbol="US.AAPL",
order_type=OrderType.LO,
side=OrderSide.Buy,
submitted_quantity=Decimal("100"),
time_in_force=TimeInForceType.Day,
submitted_price=Decimal("150.00"),
)
print(f"订单ID: {resp.order_id}")
# 修改订单
ctx.replace_order(order_id="xxx", quantity=Decimal("200"), price=Decimal("151.00"))
# 撤单
ctx.cancel_order(order_id="xxx")
```
有关 TradeContext 完整方法列表和参数说明,阅读 `references/trade-api.md`。
### 3. 资产查询
```python
from longport.openapi import TradeContext, Config
config = Config.from_env()
ctx = TradeContext(config)
# 账户余额
balances = ctx.account_balance()
for b in balances:
print(f"币种: {b.currency} 净资产: {b.net_assets} 可用: {b.total_cash}")
# 持仓
positions = ctx.stock_positions()
for ch in positions.channels:
for p in ch.positions:
print(f"{p.symbol} 数量: {p.quantity} 可用: {p.available_quantity} 成本: {p.cost_price}")
# 今日订单
orders = ctx.today_orders()
for o in orders:
print(f"[{o.status}] {o.symbol} {o.side} {o.quantity}@{o.price}")
# 今日成交
executions = ctx.today_executions()
# 历史订单
from datetime import datetime
history = ctx.history_orders(start_at=datetime(2024, 1, 1), end_at=datetime(2024, 12, 31))
# 现金流
flows = ctx.cash_flow(start_at=datetime(2024, 1, 1), end_at=datetime(2024, 12, 31))
```
### 4. 高级查询
```python
# 估算最大可买数量
from longport.openapi import OrderType, OrderSide, Decimal
resp = ctx.estimate_max_purchase_quantity(
symbol="US.AAPL",
order_type=OrderType.LO,
side=OrderSide.Buy,
price=Decimal("150.00"),
)
print(f"可买: {resp.cash_max_qty} 融资可买: {resp.margin_max_qty}")
# 保证金比率
ratio = ctx.margin_ratio("HK.00700")
print(f"初始: {ratio.im_factor} 维持: {ratio.mm_factor}")
```
## 常用订单类型
| 类型 | 枚举值 | 说明 | 适用市场 |
|------|--------|------|----------|
| 限价单 | `OrderType.LO` | 指定价格 | 港股/美股 |
| 市价单 | `OrderType.MO` | 市场价成交 | 港股/美股 |
| 增强限价单 | `OrderType.ELO` | 港股增强限价 | 港股 |
| 竞价限价单 | `OrderType.ALO` | 竞价时段 | 港股 |
| 触价限价单 | `OrderType.LIT` | 触发价+限价 | 美股 |
| 触价市价单 | `OrderType.MIT` | 触发价+市价 | 美股 |
| 跟踪止损限价(金额) | `OrderType.TSLPAMT` | 跟踪止损 | 美股 |
| 跟踪止损限价(百分比) | `OrderType.TSLPPCT` | 跟踪止损 | 美股 |
| 跟踪止损市价(金额) | `OrderType.TSMAMT` | 跟踪止损 | 美股 |
| 跟踪止损市价(百分比) | `OrderType.TSMPCT` | 跟踪止损 | 美股 |
## 输出格式
- 行情数据用表格展示,包含关键字段(价格、涨跌幅、成交量)
- 持仓数据按市场分组,显示盈亏
- 订单列表显示状态、方向、数量、价格
- 金额保留 2 位小数,百分比保留 2 位小数
## 错误处理
捕获 `OpenApiException` 并向用户展示清晰的错误信息:
```python
from longport.openapi import OpenApiException
try:
# ... API 调用
except OpenApiException as e:
print(f"API 错误: {e}")
```
常见错误:
- 环境变量未配置 → 提示用户设置
- 标的代码无效 → 检查格式是否为 `市场.代码`
- 权限不足 → 提示用户检查 API 权限
- 市场未开盘 → 告知交易时段
FILE:references/quote-api.md
# QuoteContext API 参考
## 初始化
```python
from longport.openapi import QuoteContext, Config
config = Config.from_env()
ctx = QuoteContext(config)
```
## 方法列表
### quote — 获取实时行情
```python
ctx.quote(symbols: list[str]) -> list[SecurityQuote]
```
返回值关键字段:
- `symbol`, `last_done`(最新价), `prev_close`(昨收)
- `open`, `high`, `low`, `volume`, `turnover`
- `change_rate`(涨跌幅%), `change_value`(涨跌额)
- `timestamp`
### realtime_quote — 实时报价(含盘前盘后)
```python
ctx.realtime_quote(symbols: list[str]) -> list[RealtimeQuote]
```
### static_info — 标的基础信息
```python
ctx.static_info(symbols: list[str]) -> list[SecurityStaticInfo]
```
返回:`symbol`, `name_cn`, `name_en`, `name_hk`, `exchange`, `currency`, `lot_size`, `board` 等。
### depth — 盘口数据
```python
ctx.depth(symbol: str) -> SecurityDepth
```
返回 `asks[]` 和 `bids[]`,每个包含 `price`, `volume`, `order_num`。
### realtime_depth — 实时盘口
```python
ctx.realtime_depth(symbol: str) -> SecurityDepth
```
### brokers — 经纪队列
```python
ctx.brokers(symbol: str) -> SecurityBrokers
```
### realtime_brokers — 实时经纪队列
```python
ctx.realtime_brokers(symbol: str) -> SecurityBrokers
```
### trades — 成交明细
```python
ctx.trades(symbol: str, count: int) -> list[Trade]
```
### realtime_trades — 实时成交明细
```python
ctx.realtime_trades(symbol: str, count: int = 500) -> list[Trade]
```
### candlesticks — K线数据
```python
ctx.candlesticks(
symbol: str,
period: Period,
count: int,
adjust_type: AdjustType,
) -> list[Candlestick]
```
每根 K 线包含:`open`, `close`, `high`, `low`, `volume`, `turnover`, `timestamp`。
### realtime_candlesticks — 实时K线
```python
ctx.realtime_candlesticks(symbol: str, period: Period, count: int = 500) -> list[Candlestick]
```
### history_candlesticks_by_date — 按日期查历史K线
```python
ctx.history_candlesticks_by_date(
symbol: str,
period: Period,
adjust_type: AdjustType,
start: date = None,
end: date = None,
) -> list[Candlestick]
```
### history_candlesticks_by_offset — 按偏移查历史K线
```python
ctx.history_candlesticks_by_offset(
symbol: str,
period: Period,
adjust_type: AdjustType,
forward: bool,
count: int,
time: datetime = None,
) -> list[Candlestick]
```
### intraday — 分时数据
```python
ctx.intraday(symbol: str) -> list[IntradayLine]
```
### capital_flow — 资金流向
```python
ctx.capital_flow(symbol: str) -> list[CapitalFlowLine]
```
### capital_distribution — 资金分布
```python
ctx.capital_distribution(symbol: str) -> CapitalDistributionResponse
```
### calc_indexes — 计算指标
```python
ctx.calc_indexes(
symbols: list[str],
indexes: list[CalcIndex],
) -> list[SecurityCalcIndex]
```
### option_chain_expiry_date_list — 期权到期日列表
```python
ctx.option_chain_expiry_date_list(symbol: str) -> list[date]
```
### option_chain_info_by_date — 期权链信息
```python
ctx.option_chain_info_by_date(symbol: str, expiry_date: date) -> list[StrikePriceInfo]
```
### option_quote — 期权行情
```python
ctx.option_quote(symbols: list[str]) -> list[OptionQuote]
```
### warrant_quote — 轮证行情
```python
ctx.warrant_quote(symbols: list[str]) -> list[WarrantQuote]
```
### warrant_list — 轮证筛选
```python
ctx.warrant_list(
symbol: str,
sort_by: WarrantSortBy,
sort_order: SortOrderType,
warrant_type: list[WarrantType] = None,
issuer: list[int] = None,
expiry_date: list[FilterWarrantExpiryDate] = None,
price_type: list[FilterWarrantInOutBoundsType] = None,
status: list[WarrantStatus] = None,
) -> list[WarrantInfo]
```
### warrant_issuers — 轮证发行商
```python
ctx.warrant_issuers() -> list[IssuerInfo]
```
### security_list — 标的列表
```python
ctx.security_list(market: Market, category: SecurityListCategory = None) -> list[Security]
```
### trading_session — 交易时段
```python
ctx.trading_session() -> list[MarketTradingSession]
```
### trading_days — 交易日
```python
ctx.trading_days(market: Market, begin: date, end: date) -> MarketTradingDays
```
### market_temperature — 市场温度
```python
ctx.market_temperature(market: Market) -> MarketTemperature
ctx.history_market_temperature(market: Market, start_date: date, end: date) -> HistoryMarketTemperatureResponse
```
### 自选股管理
```python
ctx.watchlist() -> list[WatchlistGroup]
ctx.create_watchlist_group(name: str, securities: list[str] = None)
ctx.update_watchlist_group(id: int, name: str = None, securities: list[str] = None, mode: SecuritiesUpdateMode = None)
ctx.delete_watchlist_group(id: int, purge: bool = False)
```
### 订阅/取消订阅
```python
ctx.subscribe(symbols: list[str], sub_types: list[SubType])
ctx.unsubscribe(symbols: list[str], sub_types: list[SubType])
ctx.subscriptions() -> list # 已订阅列表
```
### 推送回调
```python
ctx.set_on_quote(callback) # 价格推送
ctx.set_on_depth(callback) # 盘口推送
ctx.set_on_brokers(callback) # 经纪推送
ctx.set_on_trades(callback) # 成交推送
ctx.set_on_candlestick(callback) # K线推送
```
---
## 枚举值参考
### Period — K线周期
| 值 | 说明 |
|---|---|
| `Min_1` | 1分钟 |
| `Min_2` | 2分钟 |
| `Min_3` | 3分钟 |
| `Min_5` | 5分钟 |
| `Min_10` | 10分钟 |
| `Min_15` | 15分钟 |
| `Min_20` | 20分钟 |
| `Min_30` | 30分钟 |
| `Min_45` | 45分钟 |
| `Min_60` | 60分钟 |
| `Min_120` | 120分钟 |
| `Min_180` | 180分钟 |
| `Min_240` | 240分钟 |
| `Day` | 日K |
| `Week` | 周K |
| `Month` | 月K |
| `Quarter` | 季K |
| `Year` | 年K |
### AdjustType — 复权类型
| 值 | 说明 |
|---|---|
| `NoAdjust` | 不复权 |
| `ForwardAdjust` | 前复权 |
### SubType — 订阅类型
| 值 | 说明 |
|---|---|
| `Quote` | 行情报价 |
| `Depth` | 盘口 |
| `Brokers` | 经纪队列 |
| `Trade` | 成交明细 |
### TradeSession — 交易时段
| 值 | 说明 |
|---|---|
| `Intraday` | 盘中 |
| `Pre` | 盘前 |
| `Post` | 盘后 |
| `Overnight` | 夜盘 |
### WarrantSortBy — 轮证排序字段
| 值 | 说明 |
|---|---|
| `LastDone` | 最新价 |
| `ChangeRate` | 涨跌幅 |
| `ChangeValue` | 涨跌额 |
| `Volume` | 成交量 |
| `Turnover` | 成交额 |
| `ExpiryDate` | 到期日 |
| `StrikePrice` | 行使价 |
| `Premium` | 溢价 |
| `Delta` | Delta |
| `ImpliedVolatility` | 隐含波动率 |
| `EffectiveLeverage` | 有效杠杆 |
| `LeverageRatio` | 杠杆比率 |
| `ConversionRatio` | 换股比率 |
| `BalancePoint` | 打和点 |
| `CallPrice` | 收回价 |
| `ToCallPrice` | 距收回价 |
| `OutstandingQuantity` | 街货量 |
| `OutstandingRatio` | 街货比 |
| `ItmOtm` | 价内/价外 |
| `Status` | 状态 |
| `UpperStrikePrice` | 上限价 |
| `LowerStrikePrice` | 下限价 |
### WarrantType — 轮证类型
| 值 | 说明 |
|---|---|
| `Call` | 认购 |
| `Put` | 认沽 |
| `Bull` | 牛证 |
| `Bear` | 熊证 |
| `Inline` | 界内证 |
### SortOrderType — 排序方向
| 值 | 说明 |
|---|---|
| `Ascending` | 升序 |
| `Descending` | 降序 |
FILE:references/trade-api.md
# TradeContext API 参考
## 初始化
```python
from longport.openapi import TradeContext, Config
config = Config.from_env()
ctx = TradeContext(config)
```
## 方法列表
### submit_order — 提交订单
```python
ctx.submit_order(
symbol: str, # 标的代码,如 "US.AAPL"
order_type: OrderType, # 订单类型
side: OrderSide, # 买卖方向
submitted_quantity: Decimal, # 委托数量
time_in_force: TimeInForceType, # 有效期类型
submitted_price: Decimal = None, # 委托价格(限价单必填)
trigger_price: Decimal = None, # 触发价(LIT/MIT 必填)
limit_offset: Decimal = None, # 限价偏移(跟踪止损限价单)
trailing_amount: Decimal = None, # 跟踪金额(TSLPAMT/TSMAMT)
trailing_percent: Decimal = None, # 跟踪百分比(TSLPPCT/TSMPCT)
expire_date: date = None, # 到期日(GoodTilDate 时填)
outside_rth: OutsideRTH = None, # 盘前盘后(美股)
remark: str = None, # 备注
) -> SubmitOrderResponse # 返回 order_id
```
### cancel_order — 撤单
```python
ctx.cancel_order(order_id: str)
```
### replace_order — 改单
```python
ctx.replace_order(
order_id: str,
quantity: Decimal,
price: Decimal = None,
trigger_price: Decimal = None,
limit_offset: Decimal = None,
trailing_amount: Decimal = None,
trailing_percent: Decimal = None,
remark: str = None,
)
```
### today_orders — 今日订单
```python
ctx.today_orders(
symbol: str = None, # 筛选标的
status: list[OrderStatus] = None, # 筛选状态
side: OrderSide = None, # 筛选方向
market: Market = None, # 筛选市场
order_id: str = None, # 筛选订单ID
) -> list[Order]
```
### today_executions — 今日成交
```python
ctx.today_executions(
symbol: str = None,
order_id: str = None,
) -> list[Execution]
```
### history_orders — 历史订单
```python
ctx.history_orders(
symbol: str = None,
status: list[OrderStatus] = None,
side: OrderSide = None,
market: Market = None,
start_at: datetime = None,
end_at: datetime = None,
) -> list[Order]
```
### history_executions — 历史成交
```python
ctx.history_executions(
symbol: str = None,
start_at: datetime = None,
end_at: datetime = None,
) -> list[Execution]
```
### order_detail — 订单详情
```python
ctx.order_detail(order_id: str) -> OrderDetail
```
### account_balance — 账户余额
```python
ctx.account_balance(currency: str = None) -> list[AccountBalance]
```
返回值关键字段:
- `currency` — 币种
- `total_cash` — 现金总额
- `net_assets` — 净资产
- `buy_power` — 购买力
- `risk_level` — 风控等级(0安全 1中风险 2预警 3危险)
- `cash_infos` — 现金明细(available_cash, frozen_cash, settling_cash, withdraw_cash)
### stock_positions — 股票持仓
```python
ctx.stock_positions(symbols: list[str] = None) -> StockPositionsResponse
```
返回值结构:`resp.channels[].positions[]`,每个 position 包含:
- `symbol`, `quantity`, `available_quantity`
- `cost_price`, `currency`
### fund_positions — 基金持仓
```python
ctx.fund_positions(symbols: list[str] = None) -> FundPositionsResponse
```
### cash_flow — 现金流
```python
ctx.cash_flow(
start_at: datetime,
end_at: datetime,
business_type: BalanceType = None,
symbol: str = None,
page: int = None,
size: int = None,
) -> list[CashFlow]
```
### estimate_max_purchase_quantity — 估算最大可买数量
```python
ctx.estimate_max_purchase_quantity(
symbol: str,
order_type: OrderType,
side: OrderSide,
price: Decimal,
currency: str = None,
order_id: str = None,
) -> EstimateMaxPurchaseQuantityResponse
```
返回 `cash_max_qty` 和 `margin_max_qty`。
### margin_ratio — 保证金比率
```python
ctx.margin_ratio(symbol: str) -> MarginRatio
```
返回 `im_factor`(初始保证金比率)和 `mm_factor`(维持保证金比率)。
### subscribe / unsubscribe — 订阅/取消订阅订单推送
```python
ctx.subscribe(topics: list[TopicType])
ctx.unsubscribe(topics: list[TopicType])
```
### set_on_order_changed — 订单变更回调
```python
ctx.set_on_order_changed(callback)
# callback 接收 PushOrderChanged 对象
```
---
## 枚举值参考
### OrderType — 订单类型
| 值 | 说明 |
|---|---|
| `LO` | 限价单 |
| `MO` | 市价单 |
| `ELO` | 增强限价单(港股) |
| `ALO` | 竞价限价单(港股) |
| `ODD` | 碎股单(港股) |
| `SLO` | 特别限价单(港股) |
| `LIT` | 触价限价单(美股) |
| `MIT` | 触价市价单(美股) |
| `TSLPAMT` | 跟踪止损限价单-金额(美股) |
| `TSLPPCT` | 跟踪止损限价单-百分比(美股) |
| `TSMAMT` | 跟踪止损市价单-金额(美股) |
| `TSMPCT` | 跟踪止损市价单-百分比(美股) |
| `AO` | 竞价单 |
### OrderSide — 买卖方向
| 值 | 说明 |
|---|---|
| `Buy` | 买入 |
| `Sell` | 卖出 |
### TimeInForceType — 有效期
| 值 | 说明 |
|---|---|
| `Day` | 当日有效 |
| `GoodTilCanceled` | 撤单前有效 |
| `GoodTilDate` | 到期日前有效 |
### OrderStatus — 订单状态
| 值 | 说明 |
|---|---|
| `New` | 已报 |
| `Filled` | 已成交 |
| `PartialFilled` | 部分成交 |
| `Canceled` | 已撤单 |
| `Rejected` | 已拒绝 |
| `Expired` | 已过期 |
| `PendingCancel` | 待撤单 |
| `PendingReplace` | 待改单 |
| `Replaced` | 已改单 |
| `WaitToNew` | 等待报盘 |
| `WaitToReplace` | 等待改单 |
| `WaitToCancel` | 等待撤单 |
### Market — 市场
| 值 | 说明 |
|---|---|
| `US` | 美股 |
| `HK` | 港股 |
| `CN` | A股 |
| `SG` | 新加坡 |
### OutsideRTH — 盘前盘后(美股)
| 值 | 说明 |
|---|---|
| `RTHOnly` | 仅盘中 |
| `AnyTime` | 任意时段 |
| `Overnight` | 夜盘 |
### BalanceType — 余额类型
| 值 | 说明 |
|---|---|
| `Cash` | 现金 |
| `Stock` | 股票 |
| `Fund` | 基金 |
### TopicType — 推送主题
| 值 | 说明 |
|---|---|
| `Private` | 私有(订单变更) |
通过企业微信群机器人 Webhook 发送消息通知。支持文本、Markdown、Markdown V2(表格)、图片、文件消息。 支持发送到指定会话(chatid)或通过名称发送(需先注册 webhook URL)。 适用场景:服务告警、定时任务结果推送、数据报告发送、任何需要通知企业微信群的情况。
---
name: qywx-msg-sender
description: >
通过企业微信群机器人 Webhook 发送消息通知。支持文本、Markdown、Markdown V2(表格)、图片、文件消息。
支持发送到指定会话(chatid)或通过名称发送(需先注册 webhook URL)。
适用场景:服务告警、定时任务结果推送、数据报告发送、任何需要通知企业微信群的情况。
---
# 企业微信消息推送(群机器人 Webhook)
## 项目结构
```
qywx-msg-sender/
├── scripts/
│ ├── wecom_common.sh # 公共函数库(参数解析、注册表、发送封装)
│ ├── send_text.sh # 发送文本消息(支持 @成员)
│ ├── send_markdown.sh # 发送 Markdown 消息
│ ├── send_markdown_v2.sh # 发送 Markdown V2 消息(支持表格)
│ ├── send_image.sh # 发送图片消息(base64 直传,≤2MB)
│ ├── send_file.sh # 发送文件消息(先上传获取 media_id,≤20MB)
│ ├── register_chat.sh # 注册会话(绑定名称、URL、chatid)
│ ├── list_chats.sh # 列出所有已注册的会话
│ └── unregister_chat.sh # 删除已注册的会话
└── SKILL.md # 本文档
```
## 依赖要求
- `curl`(HTTP 客户端)
- `jq`(JSON 处理,版本 ≥ 1.6)
## 配置参数
所有发送脚本支持以下可选参数:
| 参数 | 环境变量 | 说明 |
|------|----------|------|
| `--to <name>` | - | 通过名称发送(推荐,需先注册) |
| `--url <url>` | `WECOM_WEBHOOK_URL` | Webhook URL |
| `--chatid <id>` | `WECOM_CHATID` | 指定会话 ID |
**优先级**:`--to`(从注册表读取 url + chatid)> `--url`/`--chatid` > 环境变量
### Webhook URL 获取
企业微信群 → 右上角「...」→ 群机器人 → 添加 → 复制 Webhook 地址
### 会话 ID (chatid) 说明
- **默认行为**:不指定 chatid 时,消息发送到创建机器人的群
- **指定会话**:通过 `--chatid` 参数可发送到其他会话
- **获取方式**:从群机器人回调事件中提取 chatid
## 会话注册表
支持通过名称发送消息,注册时绑定 webhook URL、chatid 和会话类型。
### 注册格式
```
name -> { url, chatid, chat_type }
```
- `chat_type`: `group`(群聊,默认)或 `single`(私聊)
### 注册会话
```bash
# 注册群聊(发送到 webhook 默认群)
bash scripts/register_chat.sh "研发群" "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx"
# 注册群聊(发送到指定会话)
bash scripts/register_chat.sh "告警群" "https://...?key=yyy" "wrkSFfCgAAxxxxxx"
# 注册私聊(chat_type=single)
bash scripts/register_chat.sh "张三" "https://...?key=zzz" "wokSFfCgAAyyyyyy" "single"
```
### 查看已注册
```bash
bash scripts/list_chats.sh
```
输出示例:
```
已注册的会话 (3 个):
========================================
研发群:
类型: group
URL: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx
ChatID: (默认群)
告警群:
类型: group
URL: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=yyy
ChatID: wrkSFfCgAAxxxxxx
张三:
类型: single
URL: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=zzz
ChatID: wokSFfCgAAyyyyyy
```
### 删除注册
```bash
bash scripts/unregister_chat.sh "研发群"
```
### 注册表位置
默认:`~/.wecom/chat_registry.json`
可通过环境变量自定义:
```bash
export WECOM_REGISTRY_FILE="/path/to/registry.json"
```
## 消息类型对比
| 类型 | 脚本 | 特点 | 限制 |
|------|------|------|------|
| 文本 | `send_text.sh` | 纯文本,支持 @成员 | - |
| Markdown | `send_markdown.sh` | 富文本,标题/加粗/链接等 | 4096 字节 |
| Markdown V2 | `send_markdown_v2.sh` | 支持**表格**和多行代码块 | - |
| 图片 | `send_image.sh` | base64 直传,无需上传 | 2MB,jpg/png |
| 文件 | `send_file.sh` | 自动上传获取 media_id | 20MB |
### Markdown vs Markdown V2
| 特性 | Markdown | Markdown V2 |
|------|----------|-------------|
| 基础语法 | `# 标题`、`**加粗**`、`*斜体*`、`` `代码` ``、`[链接](url)`、`> 引用` | 同左 |
| 表格 | ❌ 不支持 | ✅ 支持 |
| 多行代码块 | ❌ 不支持 | ✅ 支持 ` ``` ` |
| 内容限制 | 4096 字节 | 较宽松 |
| 兼容性 | 更好(旧版客户端) | 需较新版本 |
**选择建议**:
- 简单消息 → 用 `markdown`(轻量、兼容性好)
- 需要表格或代码块 → 必须用 `markdown_v2`
## 使用方式
所有脚本都支持 `--to`、`--url`、`--chatid` 参数。
### 文本消息
```bash
# 通过名称发送(推荐)
bash scripts/send_text.sh --to "研发群" "部署完成"
# 通过 URL 发送(临时使用)
bash scripts/send_text.sh --url "https://...?key=xxx" "消息内容"
# @ 指定成员(userid)
bash scripts/send_text.sh --to "研发群" "服务异常,请处理" "zhangsan"
# @ 所有人
bash scripts/send_text.sh --to "研发群" "紧急告警" "@all"
```
### Markdown 消息
```bash
bash scripts/send_markdown.sh --to "研发群" "## 服务状态\n**API**: 正常\n**DB**: 正常"
bash scripts/send_markdown.sh --to "研发群" "$(cat report.md)"
```
支持:`# 标题`、`**加粗**`、`*斜体*`、`` `代码` ``、`[链接](url)`、`> 引用`
### Markdown V2 消息(表格)
```bash
bash scripts/send_markdown_v2.sh --to "研发群" "## 每日数据
| 指标 | 数值 |
|------|------|
| 请求数 | 1,234 |
| 错误率 | 0.1% |
| P99 | 230ms |"
```
### 图片消息
```bash
bash scripts/send_image.sh --to "研发群" /path/to/screenshot.png
```
### 文件消息
```bash
bash scripts/send_file.sh --to "研发群" /path/to/report.xlsx
```
## 示例:服务每日报告
```bash
MSG=$(cat <<'EOF'
## 服务每日报告(2026-01-02)
**整体概况**
> 总请求: 68,019 | 活跃用户: 117 | 错误率: 0.02%
**资源消耗 Top 3**
1. app-gen-code — 6,137 次,$567
2. app-tools — 635 次,$168
3. zhangsan — 1,066 次,$97
EOF
)
bash scripts/send_markdown.sh --to "研发群" "$MSG"
```
带表格的版本(使用 markdown_v2):
```bash
MSG=$(cat <<'EOF'
## 服务每日报告(2026-01-02)
| 排名 | 名称 | 调用次数 | 消费 |
|------|------|---------|------|
| 1 | app-gen-code | 6,137 | $567 |
| 2 | app-tools | 635 | $168 |
| 3 | zhangsan | 1,066 | $97 |
EOF
)
bash scripts/send_markdown_v2.sh --to "研发群" "$MSG"
```
## 典型工作流
```bash
# 1. 首次使用:注册常用会话(绑定 webhook URL)
bash scripts/register_chat.sh "研发群" "https://...?key=xxx"
bash scripts/register_chat.sh "告警群" "https://...?key=yyy" "wrkSFfCgAAxxxxxx"
# 2. 日常使用:通过名称发送(无需记住 URL 和 chatid)
bash scripts/send_text.sh --to "研发群" "部署完成"
bash scripts/send_markdown.sh --to "告警群" "## 告警\n服务异常"
# 3. 查看已注册
bash scripts/list_chats.sh
```
FILE:scripts/list_chats.sh
#!/bin/bash
# 列出所有已注册的会话
# 用法: list_chats.sh
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
source "SCRIPT_DIR/wecom_common.sh"
list_chats
FILE:scripts/register_chat.sh
#!/bin/bash
# 注册会话:绑定名称、webhook URL、chatid 和会话类型
# 用法: register_chat.sh <name> <webhook_url> [chatid] [chat_type]
# 示例: register_chat.sh "研发群" "https://...?key=xxx"
# register_chat.sh "张三" "https://...?key=yyy" "wrkSFfCgAAxxxxxx" "single"
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
source "SCRIPT_DIR/wecom_common.sh"
if [ $# -lt 2 ]; then
echo "注册会话:绑定名称、webhook URL、chatid 和会话类型"
echo ""
echo "使用方法: $0 <name> <webhook_url> [chatid] [chat_type]"
echo ""
echo "参数说明:"
echo " name - 会话名称(用于 --to 参数)"
echo " webhook_url - 群机器人的 Webhook URL"
echo " chatid - 可选,指定会话 ID(不提供则发送到 webhook 默认群)"
echo " chat_type - 可选,会话类型:group(群聊,默认)或 single(私聊)"
echo ""
echo "示例:"
echo " # 注册群聊(发送到创建机器人的默认群)"
echo " $0 \"研发群\" \"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx\""
echo ""
echo " # 注册群聊(发送到指定会话)"
echo " $0 \"告警群\" \"https://...?key=yyy\" \"wrkSFfCgAAxxxxxx\""
echo ""
echo " # 注册私聊"
echo " $0 \"张三\" \"https://...?key=zzz\" \"wokSFfCgAAyyyyyy\" \"single\""
echo ""
echo "注册后可通过 --to 参数按名称发送:"
echo " send_text.sh --to \"研发群\" \"消息内容\""
echo " send_text.sh --to \"张三\" \"私聊消息\""
echo ""
echo "注册表位置: $WECOM_REGISTRY_FILE"
exit 1
fi
NAME="$1"
URL="$2"
CHATID="-"
CHAT_TYPE="-group"
register_chat "$NAME" "$URL" "$CHATID" "$CHAT_TYPE"
FILE:scripts/send_file.sh
#!/bin/bash
# 发送企业微信文件消息(群机器人 Webhook)
# 先上传文件获取 media_id,再发送消息
# 用法: send_file.sh [--url <url>] [--chatid <id>] [--to <name>] <file_path>
# 示例: send_file.sh /path/to/report.pdf
# send_file.sh data.xlsx
# send_file.sh --url "https://..." /path/to/report.pdf
# send_file.sh --chatid "CHATID_xxx" /path/to/report.pdf
# send_file.sh --to "研发群" /path/to/report.pdf
#
# 文件大小限制: 20MB
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
source "SCRIPT_DIR/wecom_common.sh"
# 解析参数
ARGS=$(parse_wecom_args "$@")
eval "set -- $ARGS"
if [ $# -eq 0 ]; then
echo "错误: 缺少文件路径"
echo "使用方法: $0 [--url <url>] [--chatid <id>] [--to <name>] <file_path>"
echo "示例: $0 /path/to/report.pdf"
echo " $0 --to \"研发群\" /path/to/report.pdf"
exit 1
fi
check_wecom_url
FILE_PATH="$1"
if [ ! -f "$FILE_PATH" ]; then
echo "错误: 文件不存在: FILE_PATH" >&2
exit 1
fi
# 检查文件大小(20MB 限制)
FILE_SIZE=$(wc -c < "$FILE_PATH" | tr -d ' ')
if [ "$FILE_SIZE" -gt 20971520 ]; then
echo "错误: 文件超出 20MB 限制(当前 $((FILE_SIZE / 1024 / 1024))MB)" >&2
exit 1
fi
FILENAME=$(basename "$FILE_PATH")
echo "正在上传文件: FILENAME"
# 从 URL 提取 key
WEBHOOK_KEY=$(echo "$WECOM_CURRENT_URL" | sed 's/.*key=//')
# 上传文件
UPLOAD_RESP=$(curl -s -X POST \
"https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=WEBHOOK_KEY&type=file" \
-F "media=@FILE_PATH")
ERRCODE=$(echo "$UPLOAD_RESP" | jq -r '.errcode // -1')
if [ "$ERRCODE" != "0" ]; then
ERRMSG=$(echo "$UPLOAD_RESP" | jq -r '.errmsg // "未知错误"')
echo "错误: 文件上传失败 (errcode=ERRCODE): ERRMSG" >&2
exit 1
fi
MEDIA_ID=$(echo "$UPLOAD_RESP" | jq -r '.media_id')
echo "上传成功,正在发送..."
BODY=$(jq -n \
--arg media_id "$MEDIA_ID" \
'{
msgtype: "file",
file: { media_id: $media_id }
}')
if wecom_send "$BODY"; then
echo "✅ 文件发送成功: FILENAME"
else
exit 1
fi
FILE:scripts/send_image.sh
#!/bin/bash
# 发送企业微信图片消息(群机器人 Webhook)
# 图片以 base64 编码直传,无需上传
# 用法: send_image.sh [--url <url>] [--chatid <id>] [--to <name>] <image_path>
# 示例: send_image.sh /path/to/image.png
# send_image.sh screenshot.jpg
# send_image.sh --url "https://..." /path/to/image.png
# send_image.sh --chatid "CHATID_xxx" /path/to/image.png
# send_image.sh --to "研发群" /path/to/image.png
#
# 支持格式: jpg、png
# 图片大小限制: 2MB
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
source "SCRIPT_DIR/wecom_common.sh"
# 解析参数
ARGS=$(parse_wecom_args "$@")
eval "set -- $ARGS"
if [ $# -eq 0 ]; then
echo "错误: 缺少图片路径"
echo "使用方法: $0 [--url <url>] [--chatid <id>] [--to <name>] <image_path>"
echo "示例: $0 /path/to/image.png"
echo " $0 --to \"研发群\" /path/to/image.png"
exit 1
fi
check_wecom_url
IMAGE_PATH="$1"
if [ ! -f "$IMAGE_PATH" ]; then
echo "错误: 文件不存在: IMAGE_PATH" >&2
exit 1
fi
# 检查文件大小(2MB 限制)
FILE_SIZE=$(wc -c < "$IMAGE_PATH" | tr -d ' ')
if [ "$FILE_SIZE" -gt 2097152 ]; then
echo "错误: 图片超出 2MB 限制(当前 $((FILE_SIZE / 1024))KB)" >&2
exit 1
fi
echo "正在发送图片消息: $(basename "$IMAGE_PATH")"
IMG_BASE64=$(base64 -i "$IMAGE_PATH")
IMG_MD5=$(md5 -q "$IMAGE_PATH" 2>/dev/null || md5sum "$IMAGE_PATH" | awk '{print $1}')
BODY=$(jq -n \
--arg base64 "$IMG_BASE64" \
--arg md5 "$IMG_MD5" \
'{
msgtype: "image",
image: { base64: $base64, md5: $md5 }
}')
if wecom_send "$BODY"; then
echo "✅ 图片发送成功"
else
exit 1
fi
FILE:scripts/send_markdown.sh
#!/bin/bash
# 发送企业微信 Markdown 消息(群机器人 Webhook)
# 用法: send_markdown.sh [--url <url>] [--chatid <id>] [--to <name>] <content>
# 示例: send_markdown.sh "## 告警\n服务异常"
# send_markdown.sh "$(cat report.md)"
# send_markdown.sh --url "https://..." "## 标题"
# send_markdown.sh --chatid "CHATID_xxx" "## 标题"
# send_markdown.sh --to "研发群" "## 标题"
#
# Markdown 支持: # 标题、**加粗**、*斜体*、`代码`、[链接](url)、> 引用
# 内容最长 4096 字节(UTF-8)
# 注意: 不支持表格,需要表格请用 send_markdown_v2.sh
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
source "SCRIPT_DIR/wecom_common.sh"
# 解析参数
ARGS=$(parse_wecom_args "$@")
eval "set -- $ARGS"
if [ $# -eq 0 ]; then
echo "错误: 缺少消息内容"
echo "使用方法: $0 [--url <url>] [--chatid <id>] [--to <name>] <content>"
echo "示例: $0 \"## 标题\n正文内容\""
echo " $0 --to \"研发群\" \"## 标题\""
exit 1
fi
check_wecom_url
CONTENT="$1"
# 检查内容长度
BYTE_LEN=$(echo -n "$CONTENT" | wc -c | tr -d ' ')
if [ "$BYTE_LEN" -gt 4096 ]; then
echo "错误: 内容超出 4096 字节限制(当前 BYTE_LEN 字节)" >&2
exit 1
fi
BODY=$(jq -n \
--arg content "$CONTENT" \
'{
msgtype: "markdown",
markdown: { content: $content }
}')
if wecom_send "$BODY"; then
echo "✅ 消息发送成功"
else
exit 1
fi
FILE:scripts/send_markdown_v2.sh
#!/bin/bash
# 发送企业微信 Markdown V2 消息(群机器人 Webhook)
# 支持比 markdown 更丰富的语法,包括表格和多行代码块
# 用法: send_markdown_v2.sh [--url <url>] [--chatid <id>] [--to <name>] <content>
# 示例: send_markdown_v2.sh "## 标题\n| 列1 | 列2 |\n|-----|-----|\n| a | b |"
# send_markdown_v2.sh "$(cat report.md)"
# send_markdown_v2.sh --url "https://..." "## 标题"
# send_markdown_v2.sh --chatid "CHATID_xxx" "## 标题"
# send_markdown_v2.sh --to "研发群" "## 标题"
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
source "SCRIPT_DIR/wecom_common.sh"
# 解析参数
ARGS=$(parse_wecom_args "$@")
eval "set -- $ARGS"
if [ $# -eq 0 ]; then
echo "错误: 缺少消息内容"
echo "使用方法: $0 [--url <url>] [--chatid <id>] [--to <name>] <content>"
echo "示例: $0 \"## 标题\n| 列1 | 列2 |\n|-----|-----|\n| a | b |\""
echo " $0 --to \"研发群\" \"## 标题\""
exit 1
fi
check_wecom_url
CONTENT="$1"
BODY=$(jq -n \
--arg content "$CONTENT" \
'{
msgtype: "markdown_v2",
markdown_v2: { content: $content }
}')
if wecom_send "$BODY"; then
echo "✅ 消息发送成功"
else
exit 1
fi
FILE:scripts/send_text.sh
#!/bin/bash
# 发送企业微信文本消息(群机器人 Webhook)
# 用法: send_text.sh [--url <url>] [--chatid <id>] [--to <name>] <content> [@mention_user ...]
# 示例: send_text.sh "服务器异常,请及时处理"
# send_text.sh "部署完成" "zhangsan" # @指定人
# send_text.sh "紧急告警" "@all" # @所有人
# send_text.sh --url "https://..." "消息内容" # 指定 webhook URL
# send_text.sh --chatid "CHATID_xxx" "消息" # 发送到指定会话
# send_text.sh --to "研发群" "消息" # 通过名称发送(需先注册)
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
source "SCRIPT_DIR/wecom_common.sh"
# 解析参数
ARGS=$(parse_wecom_args "$@")
eval "set -- $ARGS"
if [ $# -eq 0 ]; then
echo "错误: 缺少消息内容"
echo "使用方法: $0 [--url <url>] [--chatid <id>] [--to <name>] <content> [mention_user]"
echo "示例: $0 \"服务异常\""
echo " $0 --to \"研发群\" \"消息内容\""
echo " $0 --chatid \"CHATID_xxx\" \"消息内容\""
exit 1
fi
check_wecom_url
CONTENT="$1"
MENTION="-"
# 构建 mentioned_list
if [ "$MENTION" = "@all" ]; then
MENTIONED_LIST='["@all"]'
elif [ -n "$MENTION" ]; then
MENTIONED_LIST=$(echo "$MENTION" | tr '|' '\n' | jq -R . | jq -s .)
else
MENTIONED_LIST='[]'
fi
BODY=$(jq -n \
--arg content "$CONTENT" \
--argjson mentioned_list "$MENTIONED_LIST" \
'{
msgtype: "text",
text: {
content: $content,
mentioned_list: $mentioned_list
}
}')
if wecom_send "$BODY"; then
echo "✅ 消息发送成功"
else
exit 1
fi
FILE:scripts/unregister_chat.sh
#!/bin/bash
# 删除已注册的会话
# 用法: unregister_chat.sh <name>
# 示例: unregister_chat.sh "研发群"
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
source "SCRIPT_DIR/wecom_common.sh"
if [ $# -eq 0 ]; then
echo "删除已注册的会话"
echo ""
echo "使用方法: $0 <name>"
echo ""
echo "示例:"
echo " $0 \"研发群\""
echo ""
echo "查看已注册的会话: list_chats.sh"
exit 1
fi
NAME="$1"
unregister_chat "$NAME"
FILE:scripts/wecom_common.sh
#!/bin/bash
# 企业微信 Webhook 公共函数库
# 全局变量
WECOM_CURRENT_URL="" # 当前使用的 webhook URL
WECOM_CURRENT_CHATID="" # 当前使用的会话 ID
# 注册表文件路径(可通过环境变量覆盖)
WECOM_REGISTRY_FILE="-$HOME/.wecom/chat_registry.json"
# ============================================================================
# 注册表管理函数
# ============================================================================
# 确保注册表目录和文件存在
ensure_registry() {
local dir
dir=$(dirname "$WECOM_REGISTRY_FILE")
if [ ! -d "$dir" ]; then
mkdir -p "$dir"
fi
if [ ! -f "$WECOM_REGISTRY_FILE" ]; then
echo '{}' > "$WECOM_REGISTRY_FILE"
fi
}
# 注册 name -> { url, chatid, chat_type } 映射
# 用法: register_chat <name> <webhook_url> [chatid] [chat_type]
# chatid 可选,不提供则发送到 webhook 默认群
# chat_type 可选,single 表示私聊,group 表示群聊(默认)
register_chat() {
local name="$1"
local url="$2"
local chatid="-"
local chat_type="-group"
if [ -z "$name" ] || [ -z "$url" ]; then
echo "错误: 需要提供名称和 webhook URL" >&2
return 1
fi
ensure_registry
local tmp_file
tmp_file=$(mktemp)
if [ -n "$chatid" ]; then
jq --arg name "$name" --arg url "$url" --arg chatid "$chatid" --arg chat_type "$chat_type" \
'.[$name] = { url: $url, chatid: $chatid, chat_type: $chat_type }' "$WECOM_REGISTRY_FILE" > "$tmp_file"
echo "✅ 已注册: $name"
echo " 类型: $chat_type"
echo " URL: $url"
echo " ChatID: $chatid"
else
jq --arg name "$name" --arg url "$url" --arg chat_type "$chat_type" \
'.[$name] = { url: $url, chat_type: $chat_type }' "$WECOM_REGISTRY_FILE" > "$tmp_file"
echo "✅ 已注册: $name"
echo " 类型: $chat_type"
echo " URL: $url"
echo " ChatID: (默认群)"
fi
mv "$tmp_file" "$WECOM_REGISTRY_FILE"
}
# 删除注册
# 用法: unregister_chat <name>
unregister_chat() {
local name="$1"
if [ -z "$name" ]; then
echo "错误: 需要提供名称" >&2
return 1
fi
ensure_registry
local tmp_file
tmp_file=$(mktemp)
jq --arg name "$name" 'del(.[$name])' "$WECOM_REGISTRY_FILE" > "$tmp_file" \
&& mv "$tmp_file" "$WECOM_REGISTRY_FILE"
echo "✅ 已删除: $name"
}
# 通过名称查找注册信息
# 用法: lookup_chat <name>
# 设置全局变量 WECOM_CURRENT_URL 和 WECOM_CURRENT_CHATID
# 返回: 0 成功, 1 失败
lookup_chat() {
local name="$1"
if [ -z "$name" ]; then
return 1
fi
ensure_registry
local entry
entry=$(jq -r --arg name "$name" '.[$name] // empty' "$WECOM_REGISTRY_FILE")
if [ -z "$entry" ]; then
return 1
fi
# 提取 url 和 chatid
local url chatid
url=$(echo "$entry" | jq -r '.url // empty')
chatid=$(echo "$entry" | jq -r '.chatid // empty')
if [ -n "$url" ]; then
WECOM_CURRENT_URL="$url"
fi
if [ -n "$chatid" ]; then
WECOM_CURRENT_CHATID="$chatid"
fi
return 0
}
# 列出所有注册
# 用法: list_chats
list_chats() {
ensure_registry
local count
count=$(jq 'length' "$WECOM_REGISTRY_FILE")
if [ "$count" -eq 0 ]; then
echo "注册表为空"
return 0
fi
echo "已注册的会话 ($count 个):"
echo "========================================"
jq -r 'to_entries | .[] | "\(.key):\n 类型: \(.value.chat_type // "group")\n URL: \(.value.url)\n ChatID: \(.value.chatid // "(默认群)")\n"' "$WECOM_REGISTRY_FILE"
}
# ============================================================================
# 参数解析函数
# ============================================================================
# 解析 --url、--chatid、--to 参数,返回剩余参数
# 用法: eval "$(parse_wecom_args "$@")"
# 设置 WECOM_CURRENT_URL、WECOM_CURRENT_CHATID 并输出剩余参数
parse_wecom_args() {
local url=""
local chatid=""
local to_name=""
local args=()
while [ $# -gt 0 ]; do
case "$1" in
--url)
url="$2"
shift 2
;;
--url=*)
url="1#--url="
shift
;;
--chatid)
chatid="$2"
shift 2
;;
--chatid=*)
chatid="1#--chatid="
shift
;;
--to)
to_name="$2"
shift 2
;;
--to=*)
to_name="1#--to="
shift
;;
*)
args+=("$1")
shift
;;
esac
done
# --to 参数:通过名称查找 url 和 chatid
if [ -n "$to_name" ]; then
if ! lookup_chat "$to_name"; then
echo "错误: 未找到名称 '$to_name' 的注册,请先用 register_chat.sh 注册" >&2
exit 1
fi
# lookup_chat 已设置 WECOM_CURRENT_URL 和 WECOM_CURRENT_CHATID
fi
# 命令行参数覆盖注册表的值
if [ -n "$url" ]; then
WECOM_CURRENT_URL="$url"
elif [ -z "$WECOM_CURRENT_URL" ]; then
WECOM_CURRENT_URL="-"
fi
if [ -n "$chatid" ]; then
WECOM_CURRENT_CHATID="$chatid"
elif [ -z "$WECOM_CURRENT_CHATID" ]; then
WECOM_CURRENT_CHATID="-"
fi
# 输出剩余参数供调用方使用
printf '%q ' "args[@]"
}
# 兼容旧版函数名
parse_url_arg() {
parse_wecom_args "$@"
}
# ============================================================================
# URL 和发送函数
# ============================================================================
# 检查 webhook URL 是否已设置
check_wecom_url() {
if [ -z "$WECOM_CURRENT_URL" ]; then
echo "错误: 未指定 Webhook URL" >&2
echo "方式1: --to '<已注册名称>'" >&2
echo "方式2: --url 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx'" >&2
echo "方式3: export WECOM_WEBHOOK_URL='https://...'" >&2
echo "获取: 企业微信群 → 群设置 → 群机器人 → 添加 → 复制 Webhook 地址" >&2
exit 1
fi
}
# 兼容旧版:检查环境变量(已弃用,保留兼容)
check_wecom_env() {
if [ -z "$WECOM_CURRENT_URL" ]; then
WECOM_CURRENT_URL="-"
fi
check_wecom_url
}
# 构建发送 URL(不含 chatid,chatid 放在 body 中)
build_send_url() {
echo "-$WECOM_WEBHOOK_URL"
}
# 发送消息(底层封装)
# chatid 会自动注入到 JSON body 中
wecom_send() {
local body="$1"
local url
url=$(build_send_url)
# 如果有 chatid,注入到 JSON body 中
if [ -n "$WECOM_CURRENT_CHATID" ]; then
body=$(echo "$body" | jq --arg chatid "$WECOM_CURRENT_CHATID" '. + {chatid: $chatid}')
fi
local resp
resp=$(curl -s -X POST "$url" \
-H "Content-Type: application/json" \
-d "$body")
local errcode
errcode=$(echo "$resp" | jq -r '.errcode // -1')
if [ "$errcode" != "0" ]; then
local errmsg
errmsg=$(echo "$resp" | jq -r '.errmsg // "未知错误"')
echo "错误: 发送失败 (errcode=errcode): errmsg" >&2
return 1
fi
return 0
}
管理和安装 Claude Code skills 的工具。Use when: 列出可用skills、有哪些skills、安装skill、管理skills、添加订阅、列出订阅、查看订阅、删除订阅、取消订阅。NOT for: 与 skill 管理无关的任务。
---
name: skill-registry-manager
description: "管理和安装 Claude Code skills 的工具。Use when: 列出可用skills、有哪些skills、安装skill、管理skills、添加订阅、列出订阅、查看订阅、删除订阅、取消订阅。NOT for: 与 skill 管理无关的任务。"
---
# Skill Registry
管理和安装 Claude Code skills 的工具。维护一份常用 skills 注册表,支持订阅远程注册表或本地注册表文件,便于共享和分发 skills 列表。
## 触发条件
当用户提到以下意图时激活:
- "列出可用 skills"、"有哪些 skills"
- "安装 skill"、"装一下 xxx skill"
- "管理 skills"
- "添加订阅"、"订阅 xxx URL"
- "列出订阅"、"查看订阅"
- "删除订阅"、"取消订阅 xxx"
## 工作流程
### 1. 读取注册表
读取本技能目录下的 `registry.yaml`,解析本地 skills 和订阅列表。
#### 1.1 加载订阅注册表
遍历 `subscriptions` 列表,根据 `url` 字段判断来源类型并加载:
**判断来源类型**:
- 以 `http://` 或 `https://` 开头 → 远程注册表,使用 WebFetch 获取
- 其他情况 → 本地文件路径,按以下规则解析后使用 Read 工具读取文件内容,按 YAML 解析
**本地路径解析规则**(按优先级):
1. 展开环境变量(`$HOME`、`$HOSTNAME` 等)和 `~`
2. 如果展开后是相对路径(不以 `/` 开头),则基于**当前注册表文件所在目录**解析为绝对路径
**加载流程**:
1. **防止循环引用**: 维护已加载路径/URL 集合,跳过重复的条目
2. **递归加载**: 注册表格式统一,支持嵌套的 subscriptions(递归加载,本地和远程可互相引用)
3. **合并 skills**: 将订阅 skills 合并到总列表,为每个 skill 标注来源(本地 / 订阅名称)
4. **处理重名**: 本地 skills 优先,后订阅覆盖前订阅
5. **错误处理**: 单个订阅加载失败时记录警告,继续处理其他订阅
### 2. 展示可用列表
以带编号的表格形式展示所有可用 skills,方便用户通过编号快速选择:
```
| # | 名称 | 描述 | 来源 | 状态 |
|---|------|------|------|------|
| 1 | [skill-name](repo-url) | ... | 本地 | ... |
| 2 | [skill-name](repo-url) | ... | official | ... |
```
"名称" 列如有 `repo` 字段则渲染为链接,方便用户查看项目详情。"来源" 列显示 skill 的注册表来源:本地 skills 显示 "本地",订阅的 skills 显示订阅名称。
编号从 1 开始,按 registry.yaml 中的顺序递增。同时检查 `~/.claude/skills/` 和当前项目 `.claude/skills/` 下是否已存在同名目录,标注已安装状态。
### 3. 用户选择
使用 AskUserQuestion 工具让用户选择要安装的 skill。用户可以通过编号(如 "1")或名称指定。如果用户在触发时已经指定了名称或编号,跳过此步。
### 4. 选择安装位置
使用 AskUserQuestion 询问安装位置:
- **全局安装** (`~/.claude/skills/`) — 所有项目可用
- **项目安装** (`.claude/skills/`) — 仅当前项目可用
### 5. 执行安装
安装前检查该 skill 的 `depends` 字段。如果存在依赖且依赖的 skill 尚未安装,先按相同流程安装依赖的 skill(递归处理),全部依赖就绪后再安装目标 skill。
根据 registry.yaml 中的 `source` 类型执行安装:
- **npx**: 在目标 skills 目录下运行 install 命令
```bash
cd <target_skills_dir> && <install_command>
```
- **git**: 克隆仓库到目标 skills 目录
```bash
git clone <install_url> <target_skills_dir>/<skill_name>
```
- **local**: 复制本地目录到目标位置。`install` 路径解析规则与订阅 `url` 一致:先展开环境变量和 `~`,若为相对路径则基于**当前注册表文件所在目录**解析。
```bash
cp -r <resolved_source_path> <target_skills_dir>/<skill_name>
```
### 6. 确认结果
检查目标目录下是否存在安装后的 skill 目录,报告安装结果。
## 订阅管理
### 添加订阅
当用户触发 "添加订阅" 或 "订阅 URL" 时:
1. 获取订阅名称和 URL/路径(通过用户输入或 AskUserQuestion)
2. 验证订阅源可访问且格式正确(应包含 skills 列表):
- **远程 URL**(`http://` / `https://` 开头):使用 WebFetch 验证
- **本地文件路径**:展开环境变量(`$HOME`、`~`),使用 Read 工具验证文件存在且为合法 YAML
3. 将新订阅追加到 registry.yaml 的 subscriptions 列表(`url` 字段保留用户原始输入,如 `$HOME/xxx.yaml`)
4. 报告添加结果
### 列出订阅
当用户触发 "列出订阅" 或 "查看订阅" 时:
1. 读取 registry.yaml 中的 subscriptions 列表
2. 对每个订阅,检查可用性:
- **远程 URL**:使用 WebFetch 获取远程注册表
- **本地文件路径**:展开环境变量后使用 Read 工具检查文件是否存在且可读
3. 以表格形式展示:
```
| 名称 | URL/路径 | 类型 | 状态 | skills 数量 |
|------|----------|------|------|-------------|
| official | https://... | 远程 | 可用 | 15 |
| local-team | $HOME/team-skills.yaml | 本地 | 可用 | 8 |
| team | https://... | 远程 | 不可用 | - |
```
### 删除订阅
当用户触发 "删除订阅" 或 "取消订阅 xxx" 时:
1. 读取当前 subscriptions 列表
2. 找到匹配的订阅(按名称匹配)
3. 使用 AskUserQuestion 确认删除
4. 从 registry.yaml 中移除该订阅
5. 报告删除结果
## 注册表格式
订阅注册表采用统一的 YAML 格式,可以是远程 URL 或本地文件:
```yaml
# 可选:嵌套订阅(支持递归加载,本地和远程可混用)
subscriptions:
- name: upstream
url: https://another-registry.yaml
- name: local-shared
url: $HOME/shared-skills/registry.yaml
- name: team
url: ~/team/skills-registry.yaml
# skills 列表
skills:
- name: example-skill
description: 示例 skill
source: npx
install: "npx skills add owner/repo@skill-name -y -g"
repo: https://github.com/owner/repo/tree/main/skills/skill-name # 可选
- name: local-skill
description: 本地 skill(相对路径,基于本注册表文件所在目录)
source: local
install: ./local-skill
```
### 本地路径说明
`url`(订阅)和 `install`(local source)字段支持以下路径格式:
- `$HOME/path/to/file.yaml` — 使用 `$HOME` 环境变量
- `~/path/to/file.yaml` — 使用 `~` 简写
- `/absolute/path/to/file.yaml` — 绝对路径
- `./relative/path` 或 `../sibling/path` — 相对路径,基于当前注册表文件所在目录解析
- 路径中可包含任意环境变量,如 `$HOSTNAME`、`$USER` 等
路径展开规则:
1. 将 `~` 展开为用户主目录,将 `$VAR` 格式的环境变量通过 shell `echo` 展开为实际值
2. 展开后若为相对路径(不以 `/` 开头),则以当前注册表文件所在目录为基准拼接为绝对路径
3. registry.yaml 中保留用户原始输入的路径格式
FILE:README.md
# Skill Registry Manager
一个 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) skill,用于管理和安装其他 Claude Code skills。
维护一份常用 skills 注册表(`registry.yaml`),支持订阅远程或本地注册表,便于共享和分发 skills 列表。
## 功能
- 列出所有已注册的 skills 及其安装状态
- 支持通过编号或名称选择安装
- 支持全局安装(`~/.claude/skills/`)和项目级安装(`.claude/skills/`)
- 自动解析和安装依赖的 skills
- 支持三种安装来源:`npx`、`git`、`local`
- **订阅管理**:添加、列出、删除注册表订阅
- **嵌套订阅**:注册表可递归引用其他注册表(本地和远程可混用)
- **路径解析**:支持环境变量(`$HOME`、`$HOSTNAME`)、`~` 展开和相对路径
## 安装
```bash
# 通过 npx 安装(推荐)
cd ~/.claude/skills && npx skills add gavinyao/skill-registry-manager -y -g
# 或手动复制
cp -r . ~/.claude/skills/skill-registry-manager
# 或使用符号链接
ln -s $(pwd) ~/.claude/skills/skill-registry-manager
```
## 使用
在 Claude Code 中直接说:
- "列出可用 skills" / "有哪些 skills"
- "安装 skill-creator" / "装一下 xxx skill"
- "管理 skills"
- "添加订阅" / "订阅 xxx URL"
- "列出订阅" / "查看订阅"
- "删除订阅" / "取消订阅 xxx"
## 文件说明
| 文件 | 说明 |
|------|------|
| `SKILL.md` | Skill 定义文件,Claude Code 自动加载 |
| `registry.yaml` | 注册表配置,定义可安装的 skills 和订阅 |
| `registry.example.yaml` | 完整配置示例,涵盖所有场景 |
## 注册表格式
`registry.yaml` 采用统一的 YAML 格式(完整示例见 [`registry.example.yaml`](registry.example.yaml)):
```yaml
# 订阅其他注册表(支持递归加载,本地和远程可混用)
subscriptions:
- name: upstream
url: https://example.com/skills-registry.yaml
- name: local-shared
url: $HOME/shared-skills/registry.yaml
- name: team
url: ~/team/skills-registry.yaml
# 本地 skills 列表
skills:
- name: example-skill
description: 示例 skill
source: npx
install: "npx skills add owner/repo@skill-name -y -g"
repo: https://github.com/owner/repo # 可选,展示为链接
depends: [] # 可选,依赖的其他 skill
- name: local-skill
description: 本地 skill
source: local
install: ./local-skill # 相对路径,基于注册表文件所在目录
```
### 来源类型
| 类型 | 说明 | `install` 字段 |
|------|------|----------------|
| `npx` | 通过 npx 运行安装命令 | npx 命令 |
| `git` | 克隆 git 仓库 | 仓库 URL |
| `local` | 复制本地目录 | 本地路径(支持环境变量和相对路径) |
### 路径格式
`url`(订阅)和 `install`(local source)字段支持以下路径格式:
| 格式 | 示例 |
|------|------|
| 环境变量 | `$HOME/path/to/file.yaml` |
| `~` 简写 | `~/path/to/file.yaml` |
| 绝对路径 | `/absolute/path/to/file.yaml` |
| 相对路径 | `./relative/path`(基于注册表文件所在目录) |
### 多机器共享
通过订阅机制,可以构建分层的注册表结构,实现多机器共享和本机定制:
```
skill-registry/registry.yaml ← 入口
├── shared-registry ← 多机器共享注册表(通过 dotfiles 同步)
│ ├── skill-creator
│ ├── find-skills
│ └── 订阅 team-skills ← 团队 skills
└── host-registry ← 本机专属(按需定制)
└── (特定于本机的 skills)
```
## License
[MIT](LICENSE)
FILE:registry.example.yaml
# 示例 skills 注册表
#
# 复制此文件为 registry.yaml 并根据需要修改。
# 完整说明参见 SKILL.md 中的「注册表格式」章节。
# ──────────────────────────────────────────────
# 订阅:引用其他注册表(支持递归、嵌套)
# ──────────────────────────────────────────────
subscriptions:
# awesome-skills:社区精选 skills 合集(默认订阅)
- name: awesome
url: https://raw.githubusercontent.com/gavinyao/awesome-skills/main/registry.yaml
# 本地注册表(支持环境变量和 ~ 展开)
# - name: team-shared
# url: $HOME/shared-skills/registry.yaml
# 相对路径(基于本文件所在目录解析)
# - name: local-extra
# url: ./extra-registry.yaml
# ──────────────────────────────────────────────
# Skills 列表
# ──────────────────────────────────────────────
#
# 字段说明:
# name - skill 名称(安装后的目录名)
# description - 简要描述
# source - 安装来源类型:npx | git | local
# install - 安装命令或路径(取决于 source 类型)
# repo - 可选,项目地址(列表中渲染为链接)
# depends - 可选,依赖的其他 skill 名称列表
#
skills:
# ── npx 来源 ──
# 通过 npx 运行 skills CLI 安装
- name: skill-creator
description: 创建和改进 Claude Code skills
source: npx
install: "npx skills add anthropics/skills@skill-creator -y -g"
repo: https://github.com/anthropics/skills/tree/main/skills/skill-creator
# ── git 来源 ──
# 直接克隆 git 仓库
- name: my-team-skill
description: 团队内部 skill
source: git
install: "https://github.com/my-org/my-team-skill.git"
# ── local 来源 ──
# 复制本地目录(支持环境变量、~ 和相对路径)
- name: local-skill
description: 本地开发中的 skill
source: local
install: ./my-local-skill
# ── 带依赖的 skill ──
# depends 中的 skill 会在安装前自动检查并递归安装
- name: advanced-skill
description: 依赖其他 skill 的高级工具
source: npx
install: "npx skills add my-org/skills@advanced-skill -y -g"
depends: [skill-creator]
FILE:registry.yaml
# 订阅注册表
subscriptions:
- name: awesome
url: https://raw.githubusercontent.com/gavinyao/awesome-skills/main/registry.yaml
# 本地 skills
skills:
- name: skill-creator
description: 创建和改进 Claude Code skills
source: npx
install: "npx skills add anthropics/skills@skill-creator -y -g"
repo: https://github.com/anthropics/skills/tree/main/skills/skill-creator
- name: find-skills
description: 从 skills.sh 生态搜索和安装 skills
source: npx
install: "npx skills add vercel-labs/skills@find-skills -y -g"
repo: https://github.com/vercel-labs/skills/tree/main/skills/find-skills