@mhfire/dsh-im-bridge 0.1.2 → 0.1.3

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.en.md ADDED
@@ -0,0 +1,82 @@
1
+ [中文](./README.md) | English
2
+
3
+ # @mhfire/dsh-im-bridge
4
+
5
+ WeCom AI Bot ⇄ DeepSeek Harness Agent bridge — a **DSH plugin**.
6
+
7
+ Creates Agents **in-process** inside a dsh profile (no child-process spawn): per-sender durable sessions (the same WeCom user reuses one session with memory), sessions registered with the Web GUI (live view and continue-chat), plus a Settings → Plugins card (`allowFrom` / `agentTimeoutSec` / `startHint`, written to `settings.yaml` with hot reload).
8
+
9
+ ## Install
10
+
11
+ ### Recommended: npm package
12
+
13
+ ```powershell
14
+ dsh plugin --profile web add @mhfire/dsh-im-bridge
15
+ # Or pin a version:
16
+ # dsh plugin --profile web add @mhfire/dsh-im-bridge@0.1.2
17
+ ```
18
+
19
+ In `$DSH_HOME/profiles/web/cordis.patch.yml` (or your profile), supply credentials only (other fields ship as bundle defaults and can be overridden):
20
+
21
+ ```yaml
22
+ - id: im-bridge
23
+ config:
24
+ botId: "<your BotID>"
25
+ secret: "<your Secret>"
26
+ # optional: workspace / personaFile / … — see Configuration below
27
+ ```
28
+
29
+ Restart dsh to apply (e.g. `dsh web` / `pnpm dsh web`).
30
+
31
+ ### Optional: local development
32
+
33
+ Install from this repo’s `plugin/` directory or a `file:` path:
34
+
35
+ ```powershell
36
+ dsh plugin --profile web add <package-path>
37
+ ```
38
+
39
+ If `botId` / `secret` are missing, the plugin still loads (does not block `dsh web`); logs warn that WeCom connect is skipped. You can fill credentials in Settings → Plugins; **restart** is required to connect (this release does not hot-start the WebSocket).
40
+
41
+ ## Configuration
42
+
43
+ The bundle `cordis.patch.yml` supplies defaults for every field except `botId` / `secret`. Full field list:
44
+
45
+ | Field | Description |
46
+ |---|---|
47
+ | `botId` / `secret` | WeCom AI Bot credentials (`role('secret')`, redacted in UI); when empty, WeCom side is skipped and the host keeps running |
48
+ | `workspace` | Agent working directory (session cwd) |
49
+ | `allowFrom` | Allowed sender userids; empty = allow everyone |
50
+ | `agentTimeoutSec` | Max seconds per task; also drives progress / ETA |
51
+ | `startHint` | Placeholder text when processing starts |
52
+ | `agentPreset` | Agent preset to mount (default `standard`) |
53
+ | `persona` / `personaFile` | Bot persona (system prompt); `personaFile` wins; do not commit secrets |
54
+ | `maxReplyBytes` | Reply size cap in bytes (default 20000) |
55
+ | `deniedMessage` | Reply when the sender is not on `allowFrom` (editable in Settings) |
56
+ | `welcomeMessage` | Welcome text on `enter_chat` (editable in Settings) |
57
+ | `thinking` | Streaming “thinking” animation: `phases` / `spin` / `eggs` / `eggAfterSec` / `intervalMs` / `activityPrefix`; override via profile patch (not the simple Settings card) |
58
+
59
+ Example `thinking.phases` override:
60
+
61
+ ```yaml
62
+ thinking:
63
+ intervalMs: 1500
64
+ phases:
65
+ - atSec: 0
66
+ text: '🤔 Understanding your request…'
67
+ ```
68
+
69
+ ## Persona
70
+
71
+ `persona.md` is the bot persona: role and behavior rules, injected as a system prompt each session.
72
+ Copy `persona.example.md` to `persona.md` and edit; supports `{{model}}` / `{{cwd}}`.
73
+ The file may contain credentials and is gitignored — do not commit it.
74
+
75
+ ## Security
76
+
77
+ - Do not commit secret-bearing files such as `config.json` / `persona.md`
78
+ - Session and tool output may contain adversarial text; the plugin’s safety prompt tells the agent not to treat tool output as instructions
79
+
80
+ ## License
81
+
82
+ MIT
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ 中文 | [English](./README.en.md)
2
+
1
3
  # @mhfire/dsh-im-bridge
2
4
 
3
5
  企业微信智能机器人 ⇄ DeepSeek Harness Agent 桥接 **DSH 插件**。
@@ -6,27 +8,40 @@
6
8
 
7
9
  ## 安装
8
10
 
11
+ ### 推荐:从 npm 安装
12
+
9
13
  ```powershell
10
- dsh plugin --profile web add <本包路径>
14
+ dsh plugin --profile web add @mhfire/dsh-im-bridge
15
+ # 或钉版本:
16
+ # dsh plugin --profile web add @mhfire/dsh-im-bridge@0.1.2
11
17
  ```
12
18
 
13
- profile 的 `cordis.patch.yml` 中配置:
19
+ `$DSH_HOME/profiles/web/cordis.patch.yml`(或对应 profile)中补密钥即可(其余项已有 bundle 默认,可按需覆盖):
14
20
 
15
21
  ```yaml
16
22
  - id: im-bridge
17
23
  config:
18
24
  botId: "<你的 BotID>"
19
25
  secret: "<你的 Secret>"
20
- workspace: "<Agent 工作目录>"
21
- personaFile: "<绝对路径>/persona.md"
26
+ # 可选:workspace / personaFile 等,见下方配置项
22
27
  ```
23
28
 
24
- 重启 dsh 进程即可使用。
29
+ 重启 dsh 进程即可使用(例如 `dsh web` / `pnpm dsh web`)。
30
+
31
+ ### 备选:本地开发
32
+
33
+ 从本仓库 `plugin/` 目录或 `file:` 路径安装:
34
+
35
+ ```powershell
36
+ dsh plugin --profile web add <本包路径>
37
+ ```
25
38
 
26
39
  未配置 `botId` / `secret` 时插件仍会加载(不阻塞 `dsh web`),日志会提示跳过企微连线;Settings → 插件配置页仍可填写,保存后**重启**进程才会连接(本版本不做热启连)。
27
40
 
28
41
  ## 配置项
29
42
 
43
+ bundle 的 `cordis.patch.yml` 已为除 `botId` / `secret` 外的字段提供默认值;下表为完整说明。
44
+
30
45
  | 字段 | 说明 |
31
46
  |---|---|
32
47
  | `botId` / `secret` | 企业微信智能机器人凭证(`role('secret')`,UI 自动脱敏);缺省时跳过企微侧,不阻塞主进程 |
@@ -37,6 +52,19 @@ dsh plugin --profile web add <本包路径>
37
52
  | `agentPreset` | Agent 加入的 preset(默认 `standard`) |
38
53
  | `persona` / `personaFile` | 机器人「人设」(系统提示词);`personaFile` 优先,含敏感信息请勿入库 |
39
54
  | `maxReplyBytes` | 回复上限(字节,默认 20000) |
55
+ | `deniedMessage` | 非白名单用户的拒绝文案(Settings 可编) |
56
+ | `welcomeMessage` | 进入会话欢迎语(Settings 可编) |
57
+ | `thinking` | 流式思考动画:`phases` / `spin` / `eggs` / `eggAfterSec` / `intervalMs` / `activityPrefix`;结构请在 profile patch 覆盖,简易卡片不编辑 |
58
+
59
+ `thinking.phases` 示例(profile 覆盖时可只改文案):
60
+
61
+ ```yaml
62
+ thinking:
63
+ intervalMs: 1500
64
+ phases:
65
+ - atSec: 0
66
+ text: '🤔 正在理解你的需求…'
67
+ ```
40
68
 
41
69
  ## 人设(persona)
42
70
 
package/cordis.patch.yml CHANGED
@@ -1,6 +1,47 @@
1
- # dsh-im-bridge bundle patch: 声明插件行。
2
- # 密钥(botId/secret)等敏感配置由 profile 层($DSH_HOME/profiles/web/cordis.patch.yml)覆盖,
3
- # 不要写在这里。
1
+ # dsh-im-bridge bundle patch: 插入插件行并给出非密钥默认配置。
2
+ # botId/secret profile 层 ($DSH_HOME/profiles/<name>/cordis.patch.yml) 或 Settings 覆盖。
4
3
  - insert:
5
4
  - id: im-bridge
6
5
  name: '@mhfire/dsh-im-bridge'
6
+ config:
7
+ workspace: !!js process.cwd()
8
+ allowFrom: []
9
+ startHint: '🧠 正在思考...'
10
+ agentTimeoutSec: 600
11
+ agentPreset: standard
12
+ persona: ''
13
+ personaFile: ''
14
+ maxReplyBytes: 20000
15
+ deniedMessage: '无权访问本服务'
16
+ welcomeMessage: '👋 办公助手已就绪。直接发消息即可,例如查文件、整理文档、查资料或处理日常事务。'
17
+ thinking:
18
+ eggAfterSec: 240
19
+ intervalMs: 1500
20
+ activityPrefix: '🛠️ 正在执行 '
21
+ spin:
22
+ - '🧠'
23
+ - '💭'
24
+ - '✨'
25
+ - '🔎'
26
+ - '⚡'
27
+ eggs:
28
+ - '📎 顺手把要点整理好了,稍后一起给你'
29
+ - '📶 网络有点忙,让它慢慢跑'
30
+ - '🎯 结果快出来了,坚持一下'
31
+ - '🗂️ 资料较多,正在汇总中'
32
+ - '🌙 别盯着了,完成会自动通知你'
33
+ phases:
34
+ - atSec: 0
35
+ text: '🤔 正在理解你的需求…'
36
+ - atSec: 8
37
+ text: '📋 正在整理任务清单'
38
+ - atSec: 25
39
+ text: '🔍 正在查找相关资料'
40
+ - atSec: 55
41
+ text: '✍️ 正在处理文档/数据'
42
+ - atSec: 120
43
+ text: '🧠 正在思考最佳方案…'
44
+ - atSec: 240
45
+ text: '⏳ 任务较繁琐,请稍候…'
46
+ - atSec: 420
47
+ text: '☕ 快好了,正在收尾…'
package/lib/client.js CHANGED
@@ -18,6 +18,8 @@ var FIELDS = [
18
18
  { field: 'allowFrom', label: '允许的发送者 userid(逗号分隔,空 = 所有人)', kind: 'text' },
19
19
  { field: 'agentTimeoutSec', label: '单任务超时(秒)', kind: 'number' },
20
20
  { field: 'startHint', label: '开始处理时的占位提示', kind: 'text' },
21
+ { field: 'deniedMessage', label: '非白名单拒绝文案', kind: 'text' },
22
+ { field: 'welcomeMessage', label: '进入会话欢迎语', kind: 'text' },
21
23
  ];
22
24
 
23
25
  function fmt(f, v) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhfire/dsh-im-bridge",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "企业微信智能机器人 ⇄ DeepSeek Harness Agent 桥接插件:进程内创建 Agent(per-sender 持久会话),会话在 GUI 实时可见;含 Settings 插件配置卡片",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,7 +19,8 @@
19
19
  "src",
20
20
  "lib",
21
21
  "cordis.patch.yml",
22
- "persona.example.md"
22
+ "persona.example.md",
23
+ "README.en.md"
23
24
  ],
24
25
  "dsh": {
25
26
  "bundle": {
package/src/index.js CHANGED
@@ -19,7 +19,7 @@ import z from '@deepseek-ai/schemastery'
19
19
  import { createUserMessage } from '@deepseek-ai/dsh-llm'
20
20
  import { SessionId } from '@deepseek-ai/dsh-session'
21
21
  import { installModelSelection } from '@deepseek-ai/dsh-agent'
22
- import { startThinking, sendFinal, truncate, footerOf, fmtDuration } from './wecom.js'
22
+ import { startThinking, sendFinal, truncate, footerOf, fmtDuration, DEFAULT_THINKING } from './wecom.js'
23
23
 
24
24
  /** 稳定插件名 */
25
25
  export const name = 'im-bridge'
@@ -27,6 +27,20 @@ export const name = 'im-bridge'
27
27
  /** 依赖的核心服务 */
28
28
  export const inject = ['agents', 'sessions', 'agentDefaultModel']
29
29
 
30
+ const ThinkingPhase = z.object({
31
+ atSec: z.number(),
32
+ text: z.string(),
33
+ })
34
+
35
+ const ThinkingConfig = z.object({
36
+ phases: z.array(ThinkingPhase).default(DEFAULT_THINKING.phases),
37
+ spin: z.array(String).default(DEFAULT_THINKING.spin),
38
+ eggs: z.array(String).default(DEFAULT_THINKING.eggs),
39
+ eggAfterSec: z.number().default(DEFAULT_THINKING.eggAfterSec),
40
+ intervalMs: z.number().default(DEFAULT_THINKING.intervalMs),
41
+ activityPrefix: z.string().default(DEFAULT_THINKING.activityPrefix),
42
+ })
43
+
30
44
  /** 插件配置(zod): 敏感项由 profile 层 patch 或 Settings 提供;缺省时跳过企微连线,不阻塞主进程 */
31
45
  export const Config = z.object({
32
46
  botId: z.string().default('').role('secret'),
@@ -47,6 +61,12 @@ export const Config = z.object({
47
61
  personaFile: z.string().default(''),
48
62
  /** 回复上限(字节) */
49
63
  maxReplyBytes: z.number().default(20000),
64
+ /** 流式思考动画素材(阶段/表情/彩蛋等); 可在 profile patch 覆盖 */
65
+ thinking: ThinkingConfig.default(DEFAULT_THINKING),
66
+ /** 非白名单用户的拒绝文案 */
67
+ deniedMessage: z.string().default('无权访问本服务'),
68
+ /** 用户进入会话时的欢迎语 */
69
+ welcomeMessage: z.string().default('👋 办公助手已就绪。直接发消息即可,例如查文件、整理文档、查资料或处理日常事务。'),
50
70
  })
51
71
 
52
72
  /** 收集一次 turn 内最后一条 assistant 文本与结束原因 */
@@ -158,8 +178,9 @@ export function apply(ctx, config) {
158
178
  // 会话事件 → 真实活动状态(工具调用名), 让动画显示"正在做什么"
159
179
  ctx.on('session/event', (session, event) => {
160
180
  if (event.type !== 'tool/call') return
181
+ const prefix = cfg().thinking?.activityPrefix ?? DEFAULT_THINKING.activityPrefix
161
182
  for (const st of senders.values()) {
162
- if (st.sessionId === session.id) st.lastActivity = `🛠️ 正在执行 ${event.data.name}`
183
+ if (st.sessionId === session.id) st.lastActivity = `${prefix}${event.data.name}`
163
184
  }
164
185
  })
165
186
 
@@ -173,7 +194,11 @@ export function apply(ctx, config) {
173
194
  let stopThinking = null
174
195
  try {
175
196
  await ws.replyStream(frame, streamId, cfg().startHint, false)
176
- stopThinking = startThinking(ws, frame, streamId, startedAt, cfg().agentTimeoutSec, () => st.lastActivity || '')
197
+ stopThinking = startThinking(
198
+ ws, frame, streamId, startedAt, cfg().agentTimeoutSec,
199
+ () => st.lastActivity || '',
200
+ cfg().thinking,
201
+ )
177
202
  } catch (e) {
178
203
  console.error(`[im-bridge] 占位回复失败: ${e.message}`)
179
204
  }
@@ -217,7 +242,7 @@ export function apply(ctx, config) {
217
242
  if (!content) return
218
243
  const sender = frame.body?.sender?.userid || frame.body?.from?.userid || frame.body?.userid || 'unknown'
219
244
  if (cfg().allowFrom.length > 0 && !cfg().allowFrom.includes(sender)) {
220
- ws.replyStream(frame, generateReqId('stream'), '无权访问本服务', true).catch(() => {})
245
+ ws.replyStream(frame, generateReqId('stream'), cfg().deniedMessage, true).catch(() => {})
221
246
  return
222
247
  }
223
248
  console.log(`[im-bridge] 收到 from=${sender}: ${content.slice(0, 100)}`)
@@ -233,7 +258,7 @@ export function apply(ctx, config) {
233
258
  console.log(`[im-bridge] 用户 ${sender} 进入会话`)
234
259
  ws.replyWelcome(frame, {
235
260
  msgtype: 'text',
236
- text: { content: '👋 云PC 诊断助手已就绪。直接发消息即可让我诊断设备/服务状态。' },
261
+ text: { content: cfg().welcomeMessage },
237
262
  }).catch((e) => console.error(`[im-bridge] 欢迎语失败: ${e.message}`))
238
263
  })
239
264
 
package/src/wecom.js CHANGED
@@ -4,6 +4,30 @@
4
4
  */
5
5
  import { generateReqId } from '@wecom/aibot-node-sdk'
6
6
 
7
+ /** 流式思考动画的默认素材(可被 Config.thinking / cordis patch 覆盖) */
8
+ export const DEFAULT_THINKING = {
9
+ phases: [
10
+ { atSec: 0, text: '🤔 正在理解你的需求…' },
11
+ { atSec: 8, text: '📋 正在整理任务清单' },
12
+ { atSec: 25, text: '🔍 正在查找相关资料' },
13
+ { atSec: 55, text: '✍️ 正在处理文档/数据' },
14
+ { atSec: 120, text: '🧠 正在思考最佳方案…' },
15
+ { atSec: 240, text: '⏳ 任务较繁琐,请稍候…' },
16
+ { atSec: 420, text: '☕ 快好了,正在收尾…' },
17
+ ],
18
+ spin: ['🧠', '💭', '✨', '🔎', '⚡'],
19
+ eggs: [
20
+ '📎 顺手把要点整理好了,稍后一起给你',
21
+ '📶 网络有点忙,让它慢慢跑',
22
+ '🎯 结果快出来了,坚持一下',
23
+ '🗂️ 资料较多,正在汇总中',
24
+ '🌙 别盯着了,完成会自动通知你',
25
+ ],
26
+ eggAfterSec: 240,
27
+ intervalMs: 1500,
28
+ activityPrefix: '🛠️ 正在执行 ',
29
+ }
30
+
7
31
  /** 毫秒 → 人类可读时长(如 "9 分 47 秒") */
8
32
  export function fmtDuration(ms) {
9
33
  const s = Math.floor(ms / 1000)
@@ -37,46 +61,42 @@ export function truncate(text, max) {
37
61
  }
38
62
 
39
63
  /**
40
- * 流式动画 v3(办公助手人设): 阶段化人格台词 + 旋转表情 + 进度条 + 已用时/剩余估算 + 长任务彩蛋。
41
- * 每 1.5s 更新一次同一条流式消息(共用 streamId), 直到 agent 完成。返回停止函数。
42
- * @param {() => string} [activity] - 可选: 返回当前真实活动描述(如 "📋 正在执行 <操作>"), 覆盖阶段台词。
64
+ * 流式动画 v3: 阶段化台词 + 旋转表情 + 进度条 + 已用时/剩余估算 + 长任务彩蛋。
65
+ * 每 intervalMs 更新一次同一条流式消息(共用 streamId), 直到 agent 完成。返回停止函数。
66
+ * @param {() => string} [activity] - 可选: 返回当前真实活动描述, 覆盖阶段台词。
67
+ * @param {typeof DEFAULT_THINKING} [thinking] - 动画素材; 缺省用 {@link DEFAULT_THINKING}。
43
68
  */
44
- export function startThinking(ws, frame, streamId, startedAt, timeoutSec, activity) {
45
- const PHASES = [
46
- [0, '🤔 正在理解你的需求…'],
47
- [8, '📋 正在整理任务清单'],
48
- [25, '🔍 正在查找相关资料'],
49
- [55, '✍️ 正在处理文档/数据'],
50
- [120, '🧠 正在思考最佳方案…'],
51
- [240, '⏳ 任务较繁琐,请稍候…'],
52
- [420, '☕ 快好了,正在收尾…'],
53
- ]
54
- const SPIN = ['🧠', '💭', '✨', '🔎', '⚡']
55
- const EGGS = [
56
- '📎 顺手把要点整理好了,稍后一起给你',
57
- '📶 网络有点忙,让它慢慢跑',
58
- '🎯 结果快出来了,坚持一下',
59
- '🗂️ 资料较多,正在汇总中',
60
- '🌙 别盯着了,完成会自动通知你',
61
- ]
69
+ export function startThinking(ws, frame, streamId, startedAt, timeoutSec, activity, thinking) {
70
+ const t = { ...DEFAULT_THINKING, ...(thinking || {}) }
71
+ const phases = Array.isArray(t.phases) && t.phases.length > 0 ? t.phases : DEFAULT_THINKING.phases
72
+ const spin = Array.isArray(t.spin) && t.spin.length > 0 ? t.spin : DEFAULT_THINKING.spin
73
+ const eggs = Array.isArray(t.eggs) && t.eggs.length > 0 ? t.eggs : DEFAULT_THINKING.eggs
74
+ const eggAfterSec = Number.isFinite(t.eggAfterSec) ? t.eggAfterSec : DEFAULT_THINKING.eggAfterSec
75
+ const intervalMs = Number.isFinite(t.intervalMs) && t.intervalMs > 0 ? t.intervalMs : DEFAULT_THINKING.intervalMs
62
76
  const total = Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : 600
63
77
  let i = 0
64
78
  const timer = setInterval(() => {
65
79
  const secs = Math.floor((Date.now() - startedAt) / 1000)
66
80
  const live = activity ? activity() : ''
67
- let stage = PHASES[0][1]
68
- if (!live) for (const [t, s] of PHASES) if (secs >= t) stage = s
81
+ let stage = phases[0].text
82
+ if (!live) {
83
+ for (const p of phases) {
84
+ if (secs >= p.atSec) stage = p.text
85
+ }
86
+ }
69
87
  const pct = Math.min(Math.floor((secs / total) * 100), 99)
70
88
  const filled = '█'.repeat(Math.floor(pct / 10))
71
89
  const bar = secs < 3 ? '' : `\n${filled}${'░'.repeat(10 - filled.length)} ${String(pct).padStart(2)}%`
72
90
  const remain = Math.max(total - secs, 0)
73
91
  const remainTxt = secs < 3 ? '' : ` · 预计还剩 ${Math.floor(remain / 60)}分${remain % 60}秒`
74
- const egg = secs >= 240 ? `\n${EGGS[Math.floor(secs / 60) % EGGS.length]}` : ''
75
- const emoji = SPIN[i % SPIN.length]
92
+ const egg = secs >= eggAfterSec && eggs.length > 0
93
+ ? `\n${eggs[Math.floor(secs / 60) % eggs.length]}`
94
+ : ''
95
+ const emoji = spin[i % spin.length]
76
96
  i++
77
97
  const status = live || stage
78
98
  ws.replyStream(frame, streamId, `${emoji} ${status} ⏱ ${secs} 秒${remainTxt}${bar}${egg}`, false).catch(() => {})
79
- }, 1500)
99
+ }, intervalMs)
80
100
  return () => clearInterval(timer)
81
101
  }
82
102