@anxi78/dsh-yuanbao 0.2.0 → 0.3.0

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
@@ -1,9 +1,13 @@
1
1
  # dsh-yuanbao
2
2
 
3
+ [![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
4
+
3
5
  中文 | [English](./README_EN.md)
4
6
 
5
7
  基于 [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) (dsh) 的元宝 IM 通道插件,将元宝开放平台 WebSocket 消息接入 dsh agent runtime。
6
8
 
9
+ npm: [`@anxi78/dsh-yuanbao`](https://www.npmjs.com/package/@anxi78/dsh-yuanbao)
10
+
7
11
  > **⚠️ 本仓库是定制分支**
8
12
  >
9
13
  > 上游:[YuanbaoTeam/dsh-yuanbao](https://github.com/YuanbaoTeam/dsh-yuanbao)
@@ -17,7 +21,7 @@
17
21
  ## 与上游的差异
18
22
 
19
23
  > 对比基准:上游 `9446ebd`(fix: support yuanbao group replies)
20
- > 统计:**18 个文件,+2591 / -70 行**
24
+ > 统计:**23 个文件,+4012 / -115 行**
21
25
 
22
26
  ### 一、新增功能
23
27
 
@@ -73,6 +77,73 @@ pinnedSessionId: '' # pinned 模式下的目标会话 ID
73
77
 
74
78
  **留空自动回退到 per-peer**,不会因为配置错误把消息塞进空会话。
75
79
 
80
+ #### 5. 群聊上下文
81
+
82
+ **@bot 时能看到群里刚才聊了什么**,而不只是当前这一条。
83
+
84
+ | 项 | 说明 |
85
+ |---|---|
86
+ | `src/chat-history.ts` | 文本历史(每会话 200 条)+ 媒体历史(50 条)+ 最后一条消息追踪 |
87
+ | `src/inbound.ts` | 在 @bot 检查**之前**先记录,没 @bot 的消息也会进历史 |
88
+
89
+ 拼进上下文的格式:
90
+
91
+ ```
92
+ [群聊最近消息(按时间顺序,最后一条最旧→最新)]
93
+ Alice: 刚才那个方案怎么样
94
+ Bob: 我觉得可以
95
+ [以上是 @bot 之前的消息,下面是本次 @bot 的内容]
96
+ ```
97
+
98
+ 回复完成后按 `historyLimit` 清掉已消费的部分,避免下次重复。
99
+
100
+ 配置项:
101
+
102
+ ```yaml
103
+ historyLimit: 20 # 默认 20 条,0 = 关闭
104
+ ```
105
+
106
+ **持久化**:历史落盘到 `~/.dsh/.yuanbao-history.json`,重启不再失忆。
107
+ (只存文本,不存媒体 —— 媒体的 url 带签名时效,过期了也用不了。)
108
+
109
+ #### 6. 发图后立刻 @bot,自动带上那张图
110
+
111
+ **场景**:用户先发一张图(这条 @ 不到 bot),紧接着 @bot 问「这张图是什么」。
112
+
113
+ **触发条件(缺一不可):**
114
+
115
+ | 条件 | 说明 |
116
+ |---|---|
117
+ | **同一个人** | 上一条必须是当前发送者发的 |
118
+ | **必须是最后一条** | 中间有任何人的消息都不算,**包括自己发的文字** |
119
+ | **只对图片** | 文件不生效,图+文件混合也不生效 |
120
+
121
+ 实现要点:`chatLastMessage` 单独追踪「最后一条消息」——文本历史只记有正文的消息,
122
+ 纯图片会被跳过,所以需要独立的记录来判断上一条到底是什么。
123
+
124
+ #### 7. 引用消息(文字 + 媒体)
125
+
126
+ **引用信息不在 `msg_body`,而在消息级的 `cloud_custom_data`:**
127
+
128
+ ```json
129
+ { "quote": { "desc": "被引用的文字", "type": 2, "id": "msgId" } }
130
+ ```
131
+
132
+ `type`:1 文字 / 2 图片 / 3 文件 / 4 视频 / 5 语音。
133
+
134
+ | 引用类型 | 处理 |
135
+ |---|---|
136
+ | **文字** | 格式化为 `> [引用消息 from 某人]: 内容` |
137
+ | **媒体** | `desc` 为空,按 `id` 回查媒体历史 → 换取真实 COS 地址 → 下载 → 作为附件 |
138
+
139
+ **关键坑**:入站媒体给的是 `hunyuan.tencent.com/api/resource/download?resourceId=xxx`,
140
+ 直接 GET 返回 **401**。必须先调 `/api/resource/v1/download` 换取真正的 COS 地址。
141
+
142
+ #### 8. 设置面板持久化
143
+
144
+ 设置存到 `~/.dsh/.yuanbao-settings.json`,重启不丢。
145
+ (为什么不写 `cordis.patch.yml`:那是静态配置,改一次要手写 YAML 再重启。)
146
+
76
147
  ### 二、修复的问题
77
148
 
78
149
  #### 1. 群消息发送失败(`bot_id is required`)
@@ -142,34 +213,38 @@ Cordis 插件读取未声明的服务会抛错。新增的 `createServiceReader`
142
213
 
143
214
  ### 四、文件清单
144
215
 
145
- **新增(8 个)**
216
+ **新增(10 个)**
146
217
 
147
218
  ```
148
219
  assets/builtin-stickers.json 542 行 内置表情库
149
- client.js 219 行 设置面板
150
- src/media.ts 203 行 入站媒体
220
+ client.js 327 行 设置面板
221
+ src/media.ts 207 行 入站媒体
151
222
  src/outbound-media.ts 363 行 出站媒体
152
223
  src/services.ts 59 行 服务读取封装
153
224
  src/stickers.ts 131 行 表情包
154
225
  src/tools.ts 407 行 Agent 工具
226
+ src/chat-history.ts 363 行 文本/媒体历史、持久化
227
+ src/quote.ts 157 行 引用消息解析
155
228
  src/dsh-tools.d.ts 13 行 工具类型声明
156
229
  ```
157
230
 
158
- **修改(10 个)**
231
+ **修改(13 个)**
159
232
 
160
233
  ```
161
- src/bridge.ts +71 复用 gateway account、注册工具
162
- src/transport/sender.ts +173 发送结果传播、身份校验
163
- src/transport/outbound.ts +116 媒体消息支持
164
- src/session/manager.ts +104 会话路由
165
- src/inbound.ts +106 @提及判定、媒体提取
166
- src/index.ts +91 服务注入、配置
167
- src/gateway/index.ts +33 account 暴露、httpAuth
168
- src/config.ts +6 会话路由配置
169
- src/types.ts +8 类型定义
170
- package.json +16 client 入口声明
234
+ src/inbound.ts 主要改动 @提及判定、媒体提取、历史、自动附图
235
+ src/transport/sender.ts +265 发送结果传播、身份校验、COS 地址换取
236
+ src/transport/outbound.ts +116 媒体消息支持
237
+ src/session/manager.ts +104 会话路由
238
+ src/bridge.ts +71 复用 gateway account、注册工具
239
+ src/index.ts +91 服务注入、配置、历史恢复
240
+ src/gateway/index.ts +33 account 暴露、httpAuth
241
+ src/config.ts +10 会话路由、historyLimit
242
+ src/types.ts +10 类型定义
243
+ package.json +16 client 入口声明
171
244
  ```
172
245
 
246
+ **总计**:`23 files changed, 4012 insertions(+), 115 deletions(-)`
247
+
173
248
  ### 五、同步上游
174
249
 
175
250
  ```bash
@@ -197,14 +272,14 @@ git merge upstream/main
197
272
 
198
273
  ## 安装
199
274
 
200
- ### 方式一:从 npm registry 安装
275
+ ### 方式一:从 npm registry 安装(推荐)
201
276
 
202
277
  ```bash
203
- # 安装到独立 profile
204
- dsh plugin --profile yuanbao add dsh-yuanbao
278
+ # 安装到 web profile
279
+ dsh plugin --profile web add @anxi78/dsh-yuanbao
205
280
 
206
281
  # 启动
207
- dsh --profile yuanbao
282
+ dsh --profile web
208
283
  ```
209
284
 
210
285
  ### 方式二:本地路径安装
@@ -215,19 +290,11 @@ cd /path/to/dsh-yuanbao
215
290
  pnpm install
216
291
  pnpm build
217
292
 
218
- # 安装到 deepseek-harness profile
219
- cd /path/to/deepseek-harness
220
- pnpm dsh plugin --profile yuanbao add /path/to/dsh-yuanbao
293
+ # 安装到 web profile
294
+ dsh plugin --profile web add /path/to/dsh-yuanbao
221
295
 
222
296
  # 启动
223
- pnpm dsh --profile yuanbao
224
- ```
225
-
226
- 如果需要和 Web UI 一起使用,也可以安装到 `web` profile:
227
-
228
- ```bash
229
- pnpm dsh plugin --profile web add /path/to/dsh-yuanbao
230
- pnpm dsh --profile web
297
+ dsh --profile web
231
298
  ```
232
299
 
233
300
  ## 环境变量
@@ -372,7 +439,7 @@ agent-default-model:
372
439
  ```yaml
373
440
  - insert:
374
441
  - id: dsh-yuanbao
375
- name: 'dsh-yuanbao'
442
+ name: '@anxi78/dsh-yuanbao'
376
443
  config:
377
444
  provider: openai
378
445
  model: gpt-4o-mini
@@ -400,6 +467,7 @@ agent-default-model:
400
467
  | `sessionIdleTimeout` | number | `1800000` | 会话闲置超时,默认 30 分钟 |
401
468
  | `sessionMode` | `per-peer` / `pinned` | `per-peer` | **本分支新增**:会话路由模式 |
402
469
  | `pinnedSessionId` | string | `''` | **本分支新增**:pinned 模式下的目标会话 ID,留空回退 per-peer |
470
+ | `historyLimit` | number | `20` | **本分支新增**:群聊上下文条数,@bot 时带上最近 N 条;0 = 关闭 |
403
471
  | `processingTimeoutMs` | number | `120000` | Agent 处理超时 |
404
472
  | `maxReconnectAttempts` | number | `100` | WebSocket 最大重连次数 |
405
473
  | `access.c2cMode` | `open` / `allowlist` / `disabled` | `open` | 私聊访问控制 |
@@ -413,7 +481,7 @@ agent-default-model:
413
481
  ```yaml
414
482
  - insert:
415
483
  - id: dsh-yuanbao
416
- name: 'dsh-yuanbao'
484
+ name: '@anxi78/dsh-yuanbao'
417
485
  config:
418
486
  appKey: !!js process.env.YUANBAO_APP_KEY ?? ''
419
487
  appSecret: !!js process.env.YUANBAO_APP_SECRET ?? ''
@@ -519,10 +587,9 @@ pnpm build
519
587
  # watch
520
588
  pnpm dev
521
589
 
522
- # 本地安装到 dsh profile
523
- cd /path/to/deepseek-harness
524
- pnpm dsh plugin --profile yuanbao add /path/to/dsh-yuanbao
525
- pnpm dsh --profile yuanbao
590
+ # 本地安装到 web profile
591
+ dsh plugin --profile web add /path/to/dsh-yuanbao
592
+ dsh --profile web
526
593
  ```
527
594
 
528
595
  ## License
package/README_EN.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # dsh-yuanbao
2
2
 
3
+ [![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
4
+
3
5
  [中文](./README.md) | English
4
6
 
5
7
  A Yuanbao IM channel plugin for [deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) (dsh). It connects Yuanbao Open Platform WebSocket messages to the dsh agent runtime.
@@ -21,14 +23,14 @@ Yuanbao User → Yuanbao WebSocket Gateway → dsh-yuanbao → ctx.agents → ds
21
23
 
22
24
  ## Installation
23
25
 
24
- ### Option 1: Install from npm registry
26
+ ### Option 1: Install from npm registry (recommended)
25
27
 
26
28
  ```bash
27
- # Install into an independent profile
28
- dsh plugin --profile yuanbao add dsh-yuanbao
29
+ # Install into the web profile
30
+ dsh plugin --profile web add @anxi78/dsh-yuanbao
29
31
 
30
32
  # Start
31
- dsh --profile yuanbao
33
+ dsh --profile web
32
34
  ```
33
35
 
34
36
  ### Option 2: Install from local path
@@ -39,15 +41,14 @@ cd /path/to/dsh-yuanbao
39
41
  pnpm install
40
42
  pnpm build
41
43
 
42
- # Install into a deepseek-harness profile
43
- cd /path/to/deepseek-harness
44
- pnpm dsh plugin --profile yuanbao add /path/to/dsh-yuanbao
44
+ # Install into the web profile
45
+ dsh plugin --profile web add /path/to/dsh-yuanbao
45
46
 
46
47
  # Start
47
- pnpm dsh --profile yuanbao
48
+ dsh --profile web
48
49
  ```
49
50
 
50
- To run together with the Web UI, install it into the `web` profile:
51
+ The `web` profile is the one used together with the Web UI:
51
52
 
52
53
  ```bash
53
54
  pnpm dsh plugin --profile web add /path/to/dsh-yuanbao
@@ -196,7 +197,7 @@ You can also explicitly set the model in `cordis.patch.yml` for `dsh-yuanbao`, o
196
197
  ```yaml
197
198
  - insert:
198
199
  - id: dsh-yuanbao
199
- name: 'dsh-yuanbao'
200
+ name: '@anxi78/dsh-yuanbao'
200
201
  config:
201
202
  provider: openai
202
203
  model: gpt-4o-mini
@@ -235,7 +236,7 @@ Default bundle patch:
235
236
  ```yaml
236
237
  - insert:
237
238
  - id: dsh-yuanbao
238
- name: 'dsh-yuanbao'
239
+ name: '@anxi78/dsh-yuanbao'
239
240
  config:
240
241
  appKey: !!js process.env.YUANBAO_APP_KEY ?? ''
241
242
  appSecret: !!js process.env.YUANBAO_APP_SECRET ?? ''
@@ -326,10 +327,9 @@ pnpm build
326
327
  # Watch
327
328
  pnpm dev
328
329
 
329
- # Install locally into a dsh profile
330
- cd /path/to/deepseek-harness
331
- pnpm dsh plugin --profile yuanbao add /path/to/dsh-yuanbao
332
- pnpm dsh --profile yuanbao
330
+ # Install locally into the web profile
331
+ dsh plugin --profile web add /path/to/dsh-yuanbao
332
+ dsh --profile web
333
333
  ```
334
334
 
335
335
  ## License
package/client.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * slots.inject('settings.section', () => slots.register({...}, () => h(Panel)))
14
14
  */
15
15
  window.__ModuleLoader__.load({
16
- id: 'dsh-yuanbao',
16
+ id: '@anxi78/dsh-yuanbao',
17
17
  factory: (require) => {
18
18
  const React = require('react')
19
19
  const h = React.createElement
@@ -309,10 +309,10 @@ select.yb-input{font-family:inherit;cursor:pointer;appearance:none;background-im
309
309
  inject: ['slots', 'sessions'],
310
310
  apply(ctx) {
311
311
  const style = document.createElement('style')
312
- style.setAttribute('data-plugin-css', 'dsh-yuanbao')
312
+ style.setAttribute('data-plugin-css', '@anxi78/dsh-yuanbao')
313
313
  style.textContent = CSS
314
314
  document.head.appendChild(style)
315
- ctx.effect(() => () => style.remove(), 'dsh-yuanbao: 设置页样式')
315
+ ctx.effect(() => () => style.remove(), '@anxi78/dsh-yuanbao: 设置页样式')
316
316
 
317
317
  const slots = (typeof ctx.get === 'function' ? ctx.get('slots') : undefined) || ctx.slots
318
318
  if (!slots) return
package/cordis.patch.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  - insert:
2
2
  - id: dsh-yuanbao
3
- name: 'dsh-yuanbao'
3
+ name: '@anxi78/dsh-yuanbao'
4
4
  config:
5
5
  appKey: !!js process.env.YUANBAO_APP_KEY ?? ''
6
6
  appSecret: !!js process.env.YUANBAO_APP_SECRET ?? ''
@@ -1,15 +1,20 @@
1
1
  /**
2
- * 聊天媒体历史
3
- * =============
4
- * 元宝的「引用消息」只带被引用消息的 id 和类型,媒体类引用(图片/文件)
5
- * 的 desc 是空的。要还原出真实内容,必须靠本地缓存:
2
+ * 聊天历史
3
+ * =========
4
+ * 两类历史,生命周期不同:
6
5
  *
7
- * 收到媒体消息 → 记下 { messageId, medias[] }
8
- * 收到引用消息 → 按 quote.id 查缓存 → 找到原来的 URL 和文件名
6
+ * 1. 文本历史(chatTextHistories)
7
+ * 群里每条消息都记下来,@bot 时拼进上下文,让 agent 知道刚才在聊什么。
8
+ * 回复完成后按 limit 清空已消费的部分。
9
9
  *
10
- * 为什么要独立于消息历史:
11
- * 群聊里 @bot 之后消息历史会被清理,但引用随时可能指向更早的图片。
12
- * 媒体历史单独存,生命周期更长。
10
+ * 2. 媒体历史(chatMediaHistories)
11
+ * 记录消息携带的图片/文件,供「引用消息」按 id 回查。
12
+ * 独立存储:@bot 后文本历史会被清理,但引用随时可能指向更早的图片。
13
+ *
14
+ * 为什么要自己维护文本历史:
15
+ * 元宝只把「@bot 的那条消息」推给插件,群里的其他消息插件根本收不到。
16
+ * 等等——实际上群消息都会推送,只是没有 @ 时被 requireMention 丢掉了。
17
+ * 所以在丢弃之前先记进历史,@bot 时就能带上上下文。
13
18
  */
14
19
  import type { InboundMedia } from './media.js';
15
20
  /** 一条媒体历史记录 */
@@ -27,13 +32,80 @@ export type MediaHistoryEntry = {
27
32
  kind?: 'image' | 'file';
28
33
  }>;
29
34
  };
35
+ /** 一条文本历史记录 */
36
+ export type TextHistoryEntry = {
37
+ /** 消息 id */
38
+ messageId?: string;
39
+ /** 发送者昵称(优先)或账号 */
40
+ sender: string;
41
+ /** 发送者账号(稳定标识) */
42
+ senderId?: string;
43
+ /** 消息正文(已去除 @bot 的部分) */
44
+ body: string;
45
+ /** 消息时间戳(ms) */
46
+ timestamp: number;
47
+ };
30
48
  /** chatKey → 媒体历史 */
31
49
  export declare const chatMediaHistories: Map<string, MediaHistoryEntry[]>;
50
+ /** chatKey → 文本历史 */
51
+ export declare const chatTextHistories: Map<string, TextHistoryEntry[]>;
52
+ /**
53
+ * chatKey → 该会话收到的**最后一条消息**的元信息。
54
+ *
55
+ * 和 chatTextHistories 的区别:文本历史只记有正文的消息,
56
+ * 纯图片消息没有正文会被跳过。但「发图后立刻 @bot」需要知道
57
+ * 上一条消息**到底是什么**(哪怕它没有文字),所以单独记一份。
58
+ */
59
+ export type LastMessageEntry = {
60
+ /** 消息 id */
61
+ messageId?: string;
62
+ /** 发送者账号(稳定标识,用于判断是不是同一个人) */
63
+ senderId?: string;
64
+ /** 发送者昵称 */
65
+ senderNickname?: string;
66
+ /** 这条消息携带的媒体类型(image/file/undefined) */
67
+ mediaKinds: Array<'image' | 'file'>;
68
+ /** 媒体列表(供自动附图使用) */
69
+ medias: Array<{
70
+ url: string;
71
+ name?: string;
72
+ kind?: 'image' | 'file';
73
+ }>;
74
+ /** 记录时间 */
75
+ timestamp: number;
76
+ };
77
+ export declare const chatLastMessage: Map<string, LastMessageEntry>;
32
78
  /**
33
79
  * 生成会话级缓存键。
34
80
  * 群聊用 group:{groupCode},私聊用 direct:{fromAccount}。
35
81
  */
36
82
  export declare function deriveChatKey(isGroup: boolean, groupCode?: string, fromAccount?: string): string;
83
+ /**
84
+ * 记录一条群聊文本消息。
85
+ *
86
+ * 空正文(比如纯图片消息)不记 —— 图片走媒体历史,
87
+ * 这里记了也只是空行,反而污染上下文。
88
+ */
89
+ export declare function rememberText(chatKey: string, entry: {
90
+ messageId?: string;
91
+ sender: string;
92
+ senderId?: string;
93
+ body: string;
94
+ }): void;
95
+ /**
96
+ * 取出最近 limit 条文本历史。
97
+ *
98
+ * 返回的是**当前消息之前**的历史 —— 调用方应在记录当前消息之前调用,
99
+ * 否则当前消息会重复出现(一次在历史里,一次在正文里)。
100
+ */
101
+ export declare function recentText(chatKey: string, limit: number): TextHistoryEntry[];
102
+ /**
103
+ * 消费掉最近 limit 条历史。
104
+ *
105
+ * 回复完成后调用:这些内容已经进了 agent 的上下文,
106
+ * 下次再拼一遍就是重复。
107
+ */
108
+ export declare function consumeText(chatKey: string, limit: number): void;
37
109
  /**
38
110
  * 记录一条消息携带的媒体。
39
111
  *
@@ -50,5 +122,39 @@ export declare function rememberMedia(chatKey: string, entry: {
50
122
  * 从后往前找 —— 引用通常指向较近的消息。
51
123
  */
52
124
  export declare function findMediaByMessageId(chatKey: string, messageId: string | undefined): MediaHistoryEntry | undefined;
53
- /** 清空某个会话的历史(会话结束时调用) */
125
+ /**
126
+ * 记录「这个会话收到的最后一条消息」。
127
+ *
128
+ * 每条消息都要调(包括没有正文的纯图片消息),
129
+ * 这样「发图后立刻 @bot」才能判断上一条到底是什么。
130
+ */
131
+ export declare function rememberLastMessage(chatKey: string, entry: {
132
+ messageId?: string;
133
+ senderId?: string;
134
+ senderNickname?: string;
135
+ medias: InboundMedia[];
136
+ }): void;
137
+ /** 取这个会话的最后一条消息(没有则 undefined) */
138
+ export declare function lastMessage(chatKey: string): LastMessageEntry | undefined;
139
+ /**
140
+ * 取最近一条媒体记录。
141
+ *
142
+ * 用于「发图后立刻 @bot」的自动附图机制:需要判断上一条消息
143
+ * 是不是同一人发的图片。只看最后一条,不做模糊匹配。
144
+ */
145
+ export declare function latestMedia(chatKey: string): MediaHistoryEntry | undefined;
146
+ /** 清空某个会话的全部历史(会话结束时调用) */
54
147
  export declare function clearChatHistory(chatKey: string): void;
148
+ /** 安排一次防抖写盘(200ms 内的多次改动合并成一次) */
149
+ export declare function schedulePersist(logger?: {
150
+ warn: (m: string) => void;
151
+ }): void;
152
+ /**
153
+ * 启动时从磁盘恢复文本历史。
154
+ *
155
+ * 只恢复最近的条目,过旧的历史带进上下文反而干扰。
156
+ */
157
+ export declare function loadPersistedHistory(logger?: {
158
+ info: (m: string) => void;
159
+ warn: (m: string) => void;
160
+ }): Promise<void>;