openclaw-glance-plugin 0.1.28 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,8 +107,7 @@ await adapter.submitWatchDemand({
107
107
  variables: { threshold: 8.97 },
108
108
  channels: ['openclaw', 'email', 'call', 'sms', 'dingtalk'], // openclaw 必传,其它可选
109
109
  emailConfig: {
110
- to_address: 'demo@example.com',
111
- template_id: 4
110
+ to_address: 'demo@example.com'
112
111
  },
113
112
  callConfig: {
114
113
  phone: '13800138000',
@@ -116,12 +115,10 @@ await adapter.submitWatchDemand({
116
115
  },
117
116
  smsConfig: {
118
117
  receiver: '13800138000',
119
- template_id: 90010,
120
118
  content: '测试短信'
121
119
  },
122
120
  dingtalkConfig: {
123
121
  cas_id: 'user.dingtalk',
124
- template_id: 3,
125
122
  msg_type: 'text',
126
123
  content: '测试钉钉消息'
127
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-glance-plugin",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "OpenClaw plugin client for ticker-monitor openclaw-bridge",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: glance-watch
3
- description: 用于监控A股、港股、比特币等金融市场行情并在条件触发时发送提醒。当用户要求盯盘、监控价格、设置提醒、需要通过邮件/电话/短信/钉钉发起通知、查询A股港股指数加密与基金行情、名称查代码、交易日历、快讯时使用,例如"帮我盯着比特币"、监控某只股票、涨跌幅提醒、短信通知我、这个月交易日有哪些、有哪些新闻等。
3
+ description: 用于监控A股、港股、比特币等金融市场行情并在条件触发时发送提醒。当用户要求盯盘、监控价格、设置提醒、需要通过邮件/电话/短信/钉钉发起通知、查询A股港股指数加密标的和行情、交易日历、新闻快讯时使用,例如"帮我盯着比特币"、监控某只股票、涨跌幅提醒、短信通知我、这个月交易日有哪些、有哪些新闻等。
4
4
  ---
5
5
 
6
6
  # Glance Watch 智能盯盘(主入口)
@@ -10,16 +10,14 @@ description: 用于监控A股、港股、比特币等金融市场行情并在条
10
10
  P0(最高)策略能力:
11
11
  - 面向 **A股个股、港股个股、A股/港股指数、比特币** 创建/查询/暂停/恢复/删除盯盘策略。
12
12
  - 仅用:`watch_create` / `watch_list` / `watch_pause` / `watch_activate` / `watch_remove`。
13
- - **禁止**:对基金创建盯盘策略(包括场外基金 `xxxxxx.OF`)。
14
13
 
15
14
  P1 行情能力(为策略创建提供上下文):
16
15
  - 个股/指数/比特币实时行情:`watch_query_ticker`
17
- - 基金当日估值:`watch_query_fund_estimates`
18
16
 
19
17
  P2 标的解析能力(创建策略前补齐参数):
20
18
  - 先查本地 CSV(`skills/glance-watch/data/*.csv`)做名称/代码映射。
21
19
  - 本地不确定再用网关基础信息检索:
22
- `watch_search_a_stock_basic` / `watch_search_hk_stock_basic` / `watch_search_index_basic` / `watch_search_fund_basic`
20
+ `watch_search_a_stock_basic` / `watch_search_hk_stock_basic` / `watch_search_index_basic`
23
21
 
24
22
  P3 辅助能力:
25
23
  - 交易日历:`watch_trade_calendar`
@@ -31,11 +29,9 @@ P3 辅助能力:
31
29
  - 工具名统一使用下划线形式(如 `watch_query_ticker`),不要写成点号形式(如 `watch.query_ticker`)。
32
30
 
33
31
  - `watch_query_ticker`
34
- - `watch_query_fund_estimates`
35
32
  - `watch_search_a_stock_basic`
36
33
  - `watch_search_hk_stock_basic`
37
34
  - `watch_search_index_basic`
38
- - `watch_search_fund_basic`
39
35
  - `watch_fin_news`
40
36
  - `watch_trade_calendar`
41
37
  - `watch_create`
@@ -56,9 +52,6 @@ P3 辅助能力:
56
52
  |----------|------------|--------|
57
53
  | 现价/涨跌/几块钱(**已有代码**) | `watch_query_ticker` | `market`+`symbol`;`market` 可用 **A股/港股/加密** 等中文别名 |
58
54
  | 现价但**只有公司或指数名** | 查询`skills/glance-watch/data/*.csv`或 `watch_search_*_basic` | 从 `data[]` 取 `ts_code` 再 `watch_query_ticker` |
59
- | 基金**今天估值** | `watch_query_fund_estimates` | `fund_codes`;勿用 `watch_query_ticker` |
60
- | 基金**档案/是不是这只基** | `watch_search_fund_basic` | `ts_code` 或 `keyword` |
61
- | 要创建基金盯盘策略 | 不调用 `watch_create` | 明确告知“当前仅支持A股港股的股票/指数或比特币盯盘;基金仅支持估值查询” |
62
55
  | **新闻/快讯** | `watch_fin_news` | 必须有关键词 `keyword` 或 `q` |
63
56
  | **开不开盘/休市/交易日** | `watch_trade_calendar` | `exchange`(如 SSE/SZSE)+ `start_date`+`end_date`(单日则相同) |
64
57
 
@@ -67,8 +60,7 @@ P3 辅助能力:
67
60
  1. **盯盘策略任务**优先:`watch_create/watch_list/watch_pause/watch_activate/watch_remove`
68
61
  2. 创建策略若缺代码或市场:先本地 CSV,再 `watch_search_*_basic`
69
62
  3. 代码已明确再查实时价:`watch_query_ticker`
70
- 4. 基金只做估值/基础信息:`watch_query_fund_estimates` `watch_search_fund_basic`
71
- 5. 再处理辅助任务:`watch_trade_calendar` / `watch_fin_news` / `notify_*`
63
+ 4. 再处理辅助任务:`watch_trade_calendar` / `watch_fin_news` / `notify_*`
72
64
 
73
65
  ### 创建策略最小必填
74
66
 
@@ -79,7 +71,6 @@ P3 辅助能力:
79
71
  - `operator_parameters.variables`
80
72
 
81
73
  缺任一项先追问,不猜测阈值。
82
- 若 `product_type` 为基金(或代码为 `xxxxxx.OF`),拒绝创建并改为提供基金估值查询方案。
83
74
 
84
75
  ### 渠道选择规则(`watch_create`)
85
76
 
@@ -107,7 +98,6 @@ P3 辅助能力:
107
98
  - 把 `channel_configs.*` 传成 JSON 字符串(必须是对象)
108
99
  - 用户明确“仅/只用某几个渠道”时,仍强行附加其他渠道
109
100
  - 通过 `watch_list` 传 `user_id/use_id` 越权查询
110
- - 用 `watch_query_ticker` 查**场外基金估值**(应使用 `watch_query_fund_estimates`)
111
101
  - 对基金调用 `watch_create`
112
102
 
113
103
  ### 联系人 CSV(强约束,适用于 `watch_create` 与 `notify_*`)
@@ -127,7 +117,7 @@ P3 辅助能力:
127
117
 
128
118
  - 盯盘策略(创建/管理):`references/watch-contract.md`
129
119
  - 直连通知(短信/电话/邮件/钉钉):`references/channels.md`
130
- - 实时行情(股/指/加密)与基金估值:`references/quote-realtime.md`
120
+ - 实时行情(股/指/加密):`references/quote-realtime.md`
131
121
  - 标的检索 + 是否交易日:`references/symbol-search-and-calendar.md`
132
122
  - 快讯:`references/news-briefing.md`
133
123
  - 示例 payload:`references/examples.md`
@@ -138,7 +128,7 @@ P3 辅助能力:
138
128
  | 用户主意图 | 必读文档(最小集合) |
139
129
  |---|---|
140
130
  | 创建/管理盯盘策略 | `references/watch-contract.md` |
141
- | 查行情(股/指/加密)/ 基金估值 | `references/quote-realtime.md` |
131
+ | 查行情(股/指/加密) | `references/quote-realtime.md` |
142
132
  | 名称→代码 / 交易日判断 | `references/symbol-search-and-calendar.md` |
143
133
  | 快讯 | `references/news-briefing.md` |
144
134
  | 立即发通知 | `references/channels.md` |
@@ -160,7 +150,7 @@ P3 辅助能力:
160
150
 
161
151
  - 用户说「帮我盯 BTC 跌 2% 提醒」 -> `references/watch-contract.md` + `references/channels.md`
162
152
  - 用户说「腾讯现在多少钱」 -> `references/quote-realtime.md`(如缺代码再读 `references/symbol-search-and-calendar.md`)
163
- - 用户说「这只基金今天估值多少」 -> `references/quote-realtime.md`(`watch_query_fund_estimates`)
153
+ - 用户说「这只基金今天估值多少」 -> 明确告知“当前 OpenClaw 暂不提供基金能力”
164
154
  - 用户说「今天 A 股开不开盘」 -> `references/symbol-search-and-calendar.md`(`watch_trade_calendar`,SSE/SZSE)
165
155
  - 用户说「发短信给我」 -> `references/channels.md`
166
156
  - 用户说「有什么央行快讯」 -> `references/news-briefing.md`
@@ -21,7 +21,6 @@
21
21
 
22
22
  ### `notify_sms`
23
23
  - `receiver`(或 `phone`)
24
- - `template_id`
25
24
  - `content`
26
25
 
27
26
  ### `notify_call`
@@ -31,16 +30,32 @@
31
30
 
32
31
  ### `notify_email`
33
32
  - `to_address`
34
- - `template_id`
35
33
  - `title`
36
34
  - `content`
37
35
 
38
36
  ### `notify_dingtalk`
39
37
  - `cas_id`
40
- - `template_id`
41
38
  - `msg_type`(`text` 或 `markdown`)
42
39
  - `content`
43
40
 
41
+ ## 参数怎么填(直连通知)
42
+
43
+ - `notify_sms`
44
+ - `receiver/phone`:11 位手机号(优先用户本轮给出,否则从联系人 CSV 补)。
45
+ - `content`:一句可直接发送的短信正文,可带变量占位(如 `${price}`)。
46
+ - `notify_call`
47
+ - `phone`:11 位手机号。
48
+ - `customer_name`:称呼(优先用户输入,否则 CSV)。
49
+ - `condition`:电话播报内容,直接写“因为什么触发了通知”。
50
+ - `notify_email`
51
+ - `to_address`:收件邮箱(优先用户输入,否则 CSV)。
52
+ - `title`:邮件标题(简短,包含标的+触发事件)。
53
+ - `content`:邮件正文(包含当前值、阈值、建议动作)。
54
+ - `notify_dingtalk`
55
+ - `cas_id`:接收人钉钉账号(优先用户输入,否则 CSV 的 `dingtalk_cas_id`)。
56
+ - `msg_type`:`text`(普通文本)或 `markdown`(需要格式时)。
57
+ - `content`:消息正文。
58
+
44
59
  ## 联系人记忆(强约束)
45
60
 
46
61
  联系人 CSV:`~/.openclaw/workspace/memory/watch-notify-contacts.csv`
@@ -74,12 +89,11 @@ rg -n '^sms,jinguo\.xie,' ~/.openclaw/workspace/memory/watch-notify-contacts.csv
74
89
  ## 常见禁止项
75
90
 
76
91
  - 手机号含空格、中划线、`+86-` 等非纯数字
77
- - `template_id` 传字符串
78
92
  - `msg_type` 不是 `text/markdown`
79
93
 
80
94
  ## `watch_create` 渠道必填映射
81
95
 
82
- - 选 `email`:`channel_configs.email.to_address/template_id/title/content`
96
+ - 选 `email`:`channel_configs.email.to_address/title/content`
83
97
  - 选 `call`:`channel_configs.call.phone/customer_name/condition`
84
- - 选 `sms`:`channel_configs.sms.receiver(或phone)/template_id/content`
85
- - 选 `dingtalk`:`channel_configs.dingtalk.cas_id/template_id/msg_type/content`
98
+ - 选 `sms`:`channel_configs.sms.receiver(或phone)/content`
99
+ - 选 `dingtalk`:`channel_configs.dingtalk.cas_id/msg_type/content`
@@ -49,7 +49,6 @@ await watch_create({
49
49
  },
50
50
  sms: {
51
51
  receiver: '13800138000',
52
- template_id: 90010,
53
52
  content: '浦发银行触发盯盘条件,当前价 ${price}'
54
53
  }
55
54
  }
@@ -72,10 +71,9 @@ await watch_remove({ strategy_id: 's_123' })
72
71
  ### 2.1 发送短信(含联系人 CSV 补值)
73
72
 
74
73
  ```javascript
75
- // Step 0: 先查联系人 CSV,补 receiver/template_id
74
+ // Step 0: 先查联系人 CSV,补 receiver
76
75
  await notify_sms({
77
76
  receiver: '13800138000',
78
- template_id: 90010,
79
77
  content: '测试短信:比特币跌幅超过2%'
80
78
  })
81
79
 
@@ -93,14 +91,12 @@ await notify_call({
93
91
 
94
92
  await notify_email({
95
93
  to_address: 'demo@example.com',
96
- template_id: 4,
97
94
  title: '盯盘提醒',
98
95
  content: 'BTCUSDT 触发阈值'
99
96
  })
100
97
 
101
98
  await notify_dingtalk({
102
99
  cas_id: 'user.dingtalk',
103
- template_id: 3,
104
100
  msg_type: 'text',
105
101
  content: '盯盘触发:BTCUSDT'
106
102
  })
@@ -116,13 +112,6 @@ await watch_query_ticker({ market: 'hk', symbol: '00700', segment: 'stock' })
116
112
  await watch_query_ticker({ market: 'crypto', symbol: 'BTCUSDT' })
117
113
  ```
118
114
 
119
- ### 3.2 基金当日估值(基金不支持创建盯盘)
120
-
121
- ```javascript
122
- await watch_query_fund_estimates({ fund_codes: '000006.OF' })
123
- await watch_query_fund_estimates({ fund_codes: ['000006.OF', '110011.OF'] })
124
- ```
125
-
126
115
  ## 4) 标的检索与是否交易日查询
127
116
 
128
117
  ### 4.1 名称 -> 代码
@@ -131,7 +120,6 @@ await watch_query_fund_estimates({ fund_codes: ['000006.OF', '110011.OF'] })
131
120
  await watch_search_a_stock_basic({ keyword: '平安银行', limit: 5 })
132
121
  await watch_search_hk_stock_basic({ q: '腾讯', limit: 5 })
133
122
  await watch_search_index_basic({ keyword: '沪深300', limit: 5 })
134
- await watch_search_fund_basic({ keyword: '西部利得量化成长', limit: 5 })
135
123
  ```
136
124
 
137
125
  ### 4.2 是否交易日
@@ -158,6 +146,5 @@ await watch_fin_news({
158
146
 
159
147
  ## 常见边界提示
160
148
 
161
- - 基金(`xxxxxx.OF`)不能用 `watch_create`,只能用 `watch_query_fund_estimates` / `watch_search_fund_basic`。
162
149
  - 使用 `call/sms/email/dingtalk` 时,`watch_create` 与 `notify_*` 都要先查联系人 CSV。
163
150
  - `operator_type` 固定 `rule`,不要用其他值。
@@ -1,12 +1,10 @@
1
- # 实时行情与基金估值
1
+ # 实时行情
2
2
 
3
3
  ## 适用范围
4
4
 
5
5
  - 股票/指数/加密实时行情:`watch_query_ticker`
6
- - 基金当日估值:`watch_query_fund_estimates`
7
- - 注意:基金不支持 `watch_create` 创建盯盘策略
8
6
 
9
- ## 1) 股票/指数/加密实时行情:`watch_query_ticker`
7
+ ## 股票/指数/加密实时行情:`watch_query_ticker`
10
8
 
11
9
  必填参数:
12
10
  - `market`:`a` / `hk` / `crypto`(支持中文别名)
@@ -30,33 +28,3 @@ await watch_query_ticker({ market: 'hk', symbol: '00700', segment: 'stock' })
30
28
  await watch_query_ticker({ market: 'A股', symbol: '600000.SH' })
31
29
  await watch_query_ticker({ market: 'crypto', symbol: 'BTCUSDT' })
32
30
  ```
33
-
34
- ## 2) 基金估值:`watch_query_fund_estimates`
35
-
36
- 参数(二选一):
37
- - `fund_codes`
38
- - `fundCodes`
39
-
40
- 值格式:
41
- - 单只:`"000006.OF"`
42
- - 多只:`["000006.OF", "110011.OF"]`
43
-
44
- 成功判定:
45
- - `success === true`
46
- - `http_status === 200`
47
- - `data` 为基金代码映射
48
-
49
- 失败处理:
50
- - 读取 `error` 与 `http_status` 反馈用户
51
-
52
- 示例:
53
-
54
- ```javascript
55
- await watch_query_fund_estimates({ fund_codes: '000006.OF' })
56
- await watch_query_fund_estimates({ fund_codes: ['000006.OF', '110011.OF'] })
57
- ```
58
-
59
- ## 3) 硬边界
60
-
61
- - 不要用 `watch_query_ticker` 查基金当日估值
62
- - 不要对基金调用 `watch_create`
@@ -16,7 +16,6 @@ CSV 映射:
16
16
  - `stock_hk.csv` -> `product_type=hk_stock`
17
17
  - `index_a.csv` -> `product_type=index`
18
18
  - `index_hk.csv` -> `product_type=index`
19
- - 基金 `xxxxxx.OF` -> 仅估值/基础信息,不创建策略
20
19
 
21
20
  ### 本地 CSV 匹配算法(执行约束)
22
21
 
@@ -34,7 +33,6 @@ CSV 映射:
34
33
  - `watch_search_a_stock_basic`(A股)
35
34
  - `watch_search_hk_stock_basic`(港股)
36
35
  - `watch_search_index_basic`(指数)
37
- - `watch_search_fund_basic`(基金基础信息)
38
36
 
39
37
  统一规则:
40
38
  - 名称检索至少给 `keyword` 或 `q`
@@ -46,7 +44,6 @@ CSV 映射:
46
44
  await watch_search_a_stock_basic({ keyword: '平安银行', limit: 5 })
47
45
  await watch_search_hk_stock_basic({ q: '腾讯' })
48
46
  await watch_search_index_basic({ keyword: '沪深300' })
49
- await watch_search_fund_basic({ ts_code: '000006.OF' })
50
47
  ```
51
48
 
52
49
  ## 3) 交易日查询:`watch_trade_calendar`
@@ -13,7 +13,7 @@
13
13
  - 若报“未注册的算子类型”,将 `operator_type` 修正为 `rule` 后重试。
14
14
  - 若报 `UNSUPPORTED_PRODUCT_TYPE`,说明命中了基金边界(如 `fund` 或 `000006.OF`):
15
15
  - 不再重试 `watch_create`
16
- - 改用 `watch_query_fund_estimates` 或 `watch_search_fund_basic`
16
+ - 直接告知“当前 OpenClaw 暂不提供基金能力”
17
17
 
18
18
  ## 通知失败(`notify_*`)
19
19
 
@@ -11,8 +11,6 @@
11
11
  ## 0. 产品边界
12
12
 
13
13
  - 支持盯盘:`stock` / `hk_stock` / `index` / `crypto`
14
- - 不支持盯盘:基金(`product_type=fund` 或代码形如 `xxxxxx.OF`)
15
- - 基金相关请改用:`watch_query_fund_estimates` / `watch_search_fund_basic`
16
14
 
17
15
  ## 1. `watch_create`
18
16
 
@@ -66,19 +64,25 @@ OpenClaw 路由约束(当 `channels` 包含 `openclaw`):
66
64
  },
67
65
  dingtalk: {
68
66
  cas_id: 'jinguo.xie',
69
- template_id: 3,
70
67
  msg_type: 'text',
71
68
  content: '比特币跌幅超2%!当前价格 ${price},跌幅 ${change_percent}%。建议卖出!'
72
69
  },
73
70
  sms: {
74
71
  receiver: '13800138000',
75
- template_id: 90010,
76
72
  content: '比特币跌幅超2%!当前价格 ${price},建议卖出!'
77
73
  }
78
74
  }
79
75
  }
80
76
  ```
81
77
 
78
+ 参数填写指引(创建策略时):
79
+ - `channel_configs.sms.receiver`:手机号,优先本轮用户输入,否则联系人 CSV。
80
+ - `channel_configs.sms.content`:短信正文,写清触发条件+关键行情值。
81
+ - `channel_configs.email.to_address`:收件邮箱,优先本轮输入,否则 CSV。
82
+ - `channel_configs.email.title/content`:标题写“标的 + 触发事件”,正文写“当前值 + 阈值 + 建议动作”。
83
+ - `channel_configs.call.phone/customer_name/condition`:分别为号码、称呼、电话播报文案。
84
+ - `channel_configs.dingtalk.cas_id`:钉钉接收账号;`msg_type` 用 `text` 或 `markdown`;`content` 为消息正文。
85
+
82
86
  成功判定:
83
87
  - `success === true`
84
88
 
@@ -92,7 +96,6 @@ OpenClaw 路由约束(当 `channels` 包含 `openclaw`):
92
96
  - 顶层放 `condition`
93
97
  - `channel_configs.*` 传 JSON 字符串
94
98
  - 用户明确“仅/只用某几个渠道”时仍强行附加渠道
95
- - 基金标的调用 `watch_create`
96
99
 
97
100
  ## 联系人 CSV(创建策略场景必遵守)
98
101
 
@@ -7,12 +7,38 @@ import {
7
7
  import { OpenClawBridgeClient } from './OpenClawBridgeClient.js';
8
8
  import { extractOpenclawRoutingFromRecord } from './openclawRouting.js';
9
9
 
10
+ const CHANNEL_TEMPLATE_DEFAULTS = Object.freeze({
11
+ sms: 90010,
12
+ email: 4,
13
+ dingtalk: 3
14
+ });
15
+
10
16
  /**
11
17
  * 全局单例 Adapter 实例
12
18
  * @type {OpenClawPluginAdapter|null}
13
19
  */
14
20
  let globalAdapterInstance = null;
15
21
 
22
+ function withChannelTemplateDefaults(channel, config) {
23
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
24
+ return config;
25
+ }
26
+ const defaultTemplateId = CHANNEL_TEMPLATE_DEFAULTS[channel];
27
+ if (defaultTemplateId == null || config.template_id != null) {
28
+ return config;
29
+ }
30
+ return { ...config, template_id: defaultTemplateId };
31
+ }
32
+
33
+ function applyChannelTemplateDefaults(channelConfigs = {}) {
34
+ return {
35
+ ...channelConfigs,
36
+ sms: withChannelTemplateDefaults('sms', channelConfigs.sms),
37
+ email: withChannelTemplateDefaults('email', channelConfigs.email),
38
+ dingtalk: withChannelTemplateDefaults('dingtalk', channelConfigs.dingtalk)
39
+ };
40
+ }
41
+
16
42
  /**
17
43
  * 获取全局单例 Adapter 实例
18
44
  * @param {Object} clientOrOptions - 客户端实例或配置选项
@@ -87,13 +113,13 @@ export class OpenClawPluginAdapter {
87
113
  .filter((x) => typeof x === 'string' && x.trim())
88
114
  .map((x) => x.trim().toLowerCase())
89
115
  : [];
90
- const channelConfigs = { ...(demand.channelConfigs || {}) };
116
+ const channelConfigs = applyChannelTemplateDefaults({ ...(demand.channelConfigs || {}) });
91
117
 
92
118
  if (demand.openclawConfig) {
93
119
  if (!channels.includes('openclaw')) channels.push('openclaw');
94
120
  }
95
121
  if (demand.emailConfig) {
96
- channelConfigs.email = demand.emailConfig;
122
+ channelConfigs.email = withChannelTemplateDefaults('email', demand.emailConfig);
97
123
  if (!channels.includes('email')) channels.push('email');
98
124
  }
99
125
  if (demand.callConfig) {
@@ -101,11 +127,11 @@ export class OpenClawPluginAdapter {
101
127
  if (!channels.includes('call')) channels.push('call');
102
128
  }
103
129
  if (demand.smsConfig) {
104
- channelConfigs.sms = demand.smsConfig;
130
+ channelConfigs.sms = withChannelTemplateDefaults('sms', demand.smsConfig);
105
131
  if (!channels.includes('sms')) channels.push('sms');
106
132
  }
107
133
  if (demand.dingtalkConfig) {
108
- channelConfigs.dingtalk = demand.dingtalkConfig;
134
+ channelConfigs.dingtalk = withChannelTemplateDefaults('dingtalk', demand.dingtalkConfig);
109
135
  if (!channels.includes('dingtalk')) channels.push('dingtalk');
110
136
  }
111
137
  if (!channels.includes('openclaw')) channels.unshift('openclaw');
@@ -13,6 +13,11 @@ import { ProcessLock } from '../runtime/lock/ProcessLock.js';
13
13
  /** 与 BridgeRuntime FINANCE_TABLE_REQUEST_TIMEOUT_MS 一致 */
14
14
  const GATEWAY_TABLE_REQUEST_TIMEOUT_MS = 90_000;
15
15
  const FUND_CODE_PATTERN = /^\d{6}\.OF$/i;
16
+ const CHANNEL_TEMPLATE_DEFAULTS = Object.freeze({
17
+ sms: 90010,
18
+ email: 4,
19
+ dingtalk: 3
20
+ });
16
21
 
17
22
  let activeRuntime = null;
18
23
 
@@ -73,13 +78,13 @@ function mapDemandToCreatePayload(demand = {}) {
73
78
  .filter((x) => typeof x === 'string' && x.trim())
74
79
  .map((x) => x.trim().toLowerCase())
75
80
  : [];
76
- const channelConfigs = { ...(demand.channelConfigs || {}) };
81
+ const channelConfigs = applyChannelTemplateDefaults({ ...(demand.channelConfigs || {}) });
77
82
 
78
83
  if (demand.openclawConfig) {
79
84
  if (!channels.includes('openclaw')) channels.push('openclaw');
80
85
  }
81
86
  if (demand.emailConfig) {
82
- channelConfigs.email = demand.emailConfig;
87
+ channelConfigs.email = withChannelTemplateDefaults('email', demand.emailConfig);
83
88
  if (!channels.includes('email')) channels.push('email');
84
89
  }
85
90
  if (demand.callConfig) {
@@ -87,11 +92,11 @@ function mapDemandToCreatePayload(demand = {}) {
87
92
  if (!channels.includes('call')) channels.push('call');
88
93
  }
89
94
  if (demand.smsConfig) {
90
- channelConfigs.sms = demand.smsConfig;
95
+ channelConfigs.sms = withChannelTemplateDefaults('sms', demand.smsConfig);
91
96
  if (!channels.includes('sms')) channels.push('sms');
92
97
  }
93
98
  if (demand.dingtalkConfig) {
94
- channelConfigs.dingtalk = demand.dingtalkConfig;
99
+ channelConfigs.dingtalk = withChannelTemplateDefaults('dingtalk', demand.dingtalkConfig);
95
100
  if (!channels.includes('dingtalk')) channels.push('dingtalk');
96
101
  }
97
102
  if (!channels.includes('openclaw')) channels.unshift('openclaw');
@@ -126,29 +131,47 @@ function mapDemandToCreatePayload(demand = {}) {
126
131
 
127
132
  function mergeOpenclawChannelConfig(payload = {}, context = {}) {
128
133
  const merged = { ...(payload || {}) };
129
- const channelConfigs = { ...(merged.channel_configs || {}) };
130
- const openclawConfig = { ...(channelConfigs.openclaw || {}) };
134
+ const channelConfigs = applyChannelTemplateDefaults({ ...(merged.channel_configs || {}) });
131
135
  const routing = deriveOpenclawRouting({ params: merged, context });
136
+ merged.channel_configs = channelConfigs;
137
+
138
+ if (Object.keys(routing).length > 0) {
139
+ const openclawConfig = { ...(channelConfigs.openclaw || {}) };
140
+ channelConfigs.openclaw = {
141
+ ...openclawConfig,
142
+ ...routing
143
+ };
132
144
 
133
- if (Object.keys(routing).length === 0) {
134
- return merged;
145
+ const channels = Array.isArray(merged.channels)
146
+ ? merged.channels
147
+ .filter((x) => typeof x === 'string' && x.trim())
148
+ .map((x) => x.trim().toLowerCase())
149
+ : [];
150
+ if (!channels.includes('openclaw')) channels.unshift('openclaw');
151
+ merged.channels = channels;
135
152
  }
136
153
 
137
- channelConfigs.openclaw = {
138
- ...openclawConfig,
139
- ...routing
140
- };
154
+ return merged;
155
+ }
141
156
 
142
- const channels = Array.isArray(merged.channels)
143
- ? merged.channels
144
- .filter((x) => typeof x === 'string' && x.trim())
145
- .map((x) => x.trim().toLowerCase())
146
- : [];
147
- if (!channels.includes('openclaw')) channels.unshift('openclaw');
157
+ function withChannelTemplateDefaults(channel, config) {
158
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
159
+ return config;
160
+ }
161
+ const defaultTemplateId = CHANNEL_TEMPLATE_DEFAULTS[channel];
162
+ if (defaultTemplateId == null || config.template_id != null) {
163
+ return config;
164
+ }
165
+ return { ...config, template_id: defaultTemplateId };
166
+ }
148
167
 
149
- merged.channels = channels;
150
- merged.channel_configs = channelConfigs;
151
- return merged;
168
+ function applyChannelTemplateDefaults(channelConfigs = {}) {
169
+ return {
170
+ ...channelConfigs,
171
+ sms: withChannelTemplateDefaults('sms', channelConfigs.sms),
172
+ email: withChannelTemplateDefaults('email', channelConfigs.email),
173
+ dingtalk: withChannelTemplateDefaults('dingtalk', channelConfigs.dingtalk)
174
+ };
152
175
  }
153
176
 
154
177
  function assertWatchCreateSupported(payload = {}) {
@@ -263,7 +286,7 @@ function buildControlApi(startupPromise) {
263
286
  'notify.send requires input.channel to be one of: sms, email, call, dingtalk'
264
287
  );
265
288
  }
266
- const payload = { ...(input.payload || {}) };
289
+ const payload = withChannelTemplateDefaults(ch, { ...(input.payload || {}) });
267
290
  return runtime.request('notify.send', {
268
291
  ...payload,
269
292
  channel: ch